diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e091173..dcc1645 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,26 +2,203 @@ name: ci on: push: + branches: [main] pull_request: +permissions: + contents: read + jobs: msrv: name: msrv library check runs-on: ubuntu-latest + timeout-minutes: 10 steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.79.0 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 + with: + toolchain: 1.79.0 - run: cargo check --lib --all-features test: name: test and package runs-on: ubuntu-latest + timeout-minutes: 20 steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 + with: + toolchain: 1.93.0 + targets: thumbv7em-none-eabihf + components: rustfmt, clippy - run: git diff --check + - run: cargo fmt --check + - name: Clippy correctness and suspicious-code gates + run: cargo clippy --lib --all-features -- -A warnings -D clippy::correctness -D clippy::suspicious -D clippy::perf -D clippy::unwrap_used -D clippy::expect_used -D clippy::panic -D clippy::unreachable - run: cargo test --no-default-features - run: cargo test - run: cargo test --all-features + - run: RUSTFLAGS="-C overflow-checks=on" cargo test --release --all-features + - run: RUSTFLAGS="-C overflow-checks=off" cargo test --release --all-features + - name: No-panic fuzz soak (checked layer + pricing surface) + # Larger deterministic sweep of the public pricing surface with overflow + # checks on, so any unchecked-multiply breach fails the build hard. This + # is the continuously-run form of the safe-by-construction guarantee. + env: + SOLMATH_FUZZ_ITERS: "5000000" + RUSTFLAGS: "-C overflow-checks=on" + run: | + cargo test --release --all-features --test checked_layer -- --nocapture + cargo test --release --all-features --test critical_invariants -- --nocapture + - run: cargo check --target thumbv7em-none-eabihf --lib --all-features + - name: Verify the public feature contract + run: python3 scripts/verify_feature_contract.py + - name: Check every feature independently + run: | + while IFS= read -r feature; do + cargo check --lib --no-default-features --features "$feature" + done < <(python3 scripts/verify_feature_contract.py --list) - run: cargo check --examples --all-features + - run: RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps + - run: ./scripts/verify_critical_invariants.sh - run: cargo package + - name: Test the exact published source tree + run: | + package_version="$(cargo metadata --no-deps --format-version 1 | python3 -c 'import json, sys; print(json.load(sys.stdin)["packages"][0]["version"])')" + cargo test --manifest-path "target/package/solmath-${package_version}/Cargo.toml" --all-features + - name: Verify published package surface + run: | + package_files="$(cargo package --list)" + for required in \ + src/american_kbi.rs \ + src/american_kbi_data.rs \ + docs/AMERICAN_KBI.md \ + USAGE.md \ + VALIDATION.md \ + examples/american_kbi_batch.rs \ + docs/NIG.md; do + if ! grep -Fxq "$required" <<<"$package_files"; then + echo "required release asset missing from crate: $required" >&2 + exit 1 + fi + done + for excluded in \ + benchmark/prod_ln_vectors.json \ + benchmark/adv_ln_vectors.json \ + benchmark/prod_ln_1p_vectors.json \ + benchmark/adv_ln_1p_vectors.json \ + benchmark/prod_expm1_vectors.json \ + benchmark/adv_expm1_vectors.json \ + benchmark/prod_exp_vectors.json \ + benchmark/adv_exp_vectors.json \ + benchmark/prod_norm_cdf_vectors.json \ + benchmark/adv_norm_cdf_vectors.json \ + benchmark/asian_quantlib_vectors.json \ + benchmark/american_kbi_runtime_accuracy_report.json \ + benchmark/american_kbi_unseen_accuracy_report.json \ + benchmark/american_kbi_release_report.json \ + benchmark/nig_independent_oracle_report.json \ + benchmark/nig_cu_report.json \ + benchmark/nig_footprint_report.json \ + benchmark/nig_release_report.json \ + benchmark/sbf-footprint/Cargo.toml \ + benchmark/sbf-composite/Cargo.toml \ + benchmark/sbf-composite/package.json \ + benchmark/sbf-composite/package-lock.json \ + PROOFS.md \ + examples/README.md \ + examples/anchor_options_pricing.md \ + src/american_rom.rs \ + src/american_rom_data.rs \ + src/american_rom_operator_data.rs \ + docs/AMERICAN_ROM.md \ + examples/american_rom_batch.rs; do + if grep -Fxq "$excluded" <<<"$package_files"; then + echo "repository-only asset leaked into crate: $excluded" >&2 + exit 1 + fi + done + for excluded in \ + examples/american_closed_form_batch.rs \ + src/american.rs \ + src/phi2_bs2002.rs \ + benchmark/american_kbi_vs_closed_form_report.json \ + benchmark/american_kbi_vs_closed_form_cu_report.json \ + examples/american_volterra_batch.rs \ + src/american_volterra.rs; do + if grep -Fxq "$excluded" <<<"$package_files"; then + echo "obsolete compatibility asset leaked into crate: $excluded" >&2 + exit 1 + fi + done + - name: Enforce compact crate package + run: | + package_version="$(cargo metadata --no-deps --format-version 1 | python3 -c 'import json, sys; print(json.load(sys.stdin)["packages"][0]["version"])')" + package_path="target/package/solmath-${package_version}.crate" + package_bytes="$(wc -c <"$package_path")" + package_limit=$((260 * 1024)) + echo "package: $package_bytes bytes (limit: $package_limit)" + if [ "$package_bytes" -gt "$package_limit" ]; then + echo "published crate exceeds the 260 KiB size ceiling" >&2 + exit 1 + fi + + numerical-certificates: + name: rigorous numerical certificates + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 + with: + python-version: "3.12" + - name: Install hash-locked certificate dependencies + run: >- + python3 -m pip install --disable-pip-version-check + --only-binary=:all: --require-hashes + -r scripts/requirements-certificates.txt + - run: python3 scripts/generate_american_kbi_data.py --check src/american_kbi_data.rs + - run: python3 scripts/certify_ln_fixed.py + - run: python3 scripts/certify_exp_fixed.py + - run: python3 scripts/certify_norm_cdf.py + + formal-verification: + name: bit-precise Kani proofs + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false + - uses: model-checking/kani-github-action@f838096619a707b0f6b2118cf435eaccfa33e51f + with: + kani-version: "0.67.0" + args: "--features full" + + dependency-audit: + name: dependency audit + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 + with: + toolchain: 1.93.0 + - run: cargo install cargo-audit --locked --version 0.22.0 + - run: cargo audit + - name: Audit repository-only SBF harnesses + run: | + cargo audit --file benchmark/sbf-footprint/Cargo.lock + cargo audit --file benchmark/sbf-composite/Cargo.lock + - name: Audit repository-only metering client + run: | + npm ci --prefix benchmark/sbf-composite --ignore-scripts --no-audit --no-fund + npm audit --prefix benchmark/sbf-composite --omit=dev --ignore-scripts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b880552 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +/target +Cargo.lock +**/.DS_Store +/test-ledger/ +/.superstack/ +!/benchmark/sbf-footprint/Cargo.lock +/benchmark/sbf-footprint/target/ +!/benchmark/sbf-composite/Cargo.lock +/benchmark/sbf-composite/target/ +/benchmark/sbf-composite/node_modules/ +/benchmark/prod_*.json +/benchmark/adv_*.json +/benchmark/asian_quantlib_vectors.json +PROOFS.tex +.env +.env.* +!.env.example +!.env.sample +*.pem +*.key +*keypair*.json +*wallet*.json +*seed*.json +scripts/__pycache__/ +*.pyc diff --git a/CHANGELOG.md b/CHANGELOG.md index 64113b3..8d0c76c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,174 @@ # Changelog +## 0.2.0 - 2026-07-14 + +- Made the feature graph an explicit release contract. The default surface is + now core arithmetic plus `transcendental`; complex arithmetic and every + pricing model are opt-in, while `full` includes stable runtime capabilities + but deliberately excludes offline `table-gen` and experimental `pade-iv`. + Removed the inert root `idl-build` flag. These feature/API changes are + intentionally released as `0.2.0`, rather than a semver-invalid `0.1.6`. +- Kept generated validation reports in the tagged repository instead of the + crates.io source archive. The published crate contains the implementation, + user documentation, examples, and measured summaries without spending its + package-size budget on machine-readable release evidence. +- Expanded bit-precise formal verification from 2 to 13 Kani harnesses and from + 90 to 545 successful checks. In addition to complete-domain ULP rounding and + double-word invariants, production-linked proofs now cover exact U256 product + carry assembly, u64 quotient-digit fit, Knuth-D3's two-correction bound, and + exact square-root Newton and bracket transitions. Fixed-step square-root + results now use a one-square release certificate and restart through the + exact monotonically convergent integer-Newton kernel if it rejects. + +- Replaced the temporary fail-closed NIG stubs with a fully on-chain, + domain-gated exponential-NIG call/put engine. It prices the OTM leg through a + 15/7 direct-density rule, derives the other leg by parity, uses a capped + Chernoff tail certificate, and exposes signed rates/dividends plus a + quote-local error allowance. A generated piecewise scaled-Bessel kernel cut + the initial 31-node prototype from roughly 1.03M CU to a 381,385-CU deployed + maximum. The final 100K/10K campaign has zero allowance/request violations + and `$0.000565/$0.001397` per `$100` normalized maxima; independent 50-digit + density and Lewis oracles agree to `1.8e-16` dollars. Isolated linked size is + 311,464 bytes, a 126,616-byte delta over the Anchor baseline. Repository-only + generators and toolchain reports stay out of the published source archive, + which remains below 259 KiB against a 260 KiB CI ceiling. + +- Completed a release red-team pass: removed the unpublished Volterra + compatibility feature/API/example, removed the BS1993/BS2002/CRR American + feature, API, validation harnesses, and 96 KiB bivariate table, removed the + public KBI profiling hook, reduced the old NIG implementations to temporary + fail-closed stubs before the replacement above, and stopped deterministic Heston from pulling + the complex/stochastic research engine into production builds. KBI is now + the crate's only American-option implementation. Rebuilding the full-feature + SBF composite reduced it from 1,040,392 to 1,012,056 bytes; at that + intermediate fail-closed point the isolated KBI artifact was 291,544 bytes. A + clean package rebuild was roughly 33 KiB smaller. +- Fixed the published-source test boundary by moving the generated SABR corpus + to a repository-only integration test. The unpacked crate now passes its own + all-feature test suite without shipping the large generated corpus. +- Bumped the release candidate from the already-published `0.1.5` to `0.2.0`, + added docs.rs metadata, and tightened the CI package/feature checks. + +- Removed the experimental `american-rom` runtime, feature, generated payload, + example, and benchmark instructions. KBI is now the sole reduced-cost + accuracy-first American integration path. The published package excludes + repository-only validation corpora and is guarded by a 260 KiB CI ceiling. + +- Added `american-kbi`, an accuracy-first, fully on-chain American call/put + pricer using nonlinear smooth-pasting boundary reconstruction and Kim's + early-exercise-premium integration. A singularity-cancelling six-node + Gaussian history rule and nine positive QdFp-regularized cubature weights + retain all six-input-dependent work in-program. Against QuantLib 1.41 QdFp, + unseen maxima are $0.002502/$0.002698 per $100 strike for calls/puts. A full, + unsampled 100K production plus 10K adversarial campaign accepted every + in-domain quote and measured production maxima of $0.002744/$0.003264. + Deployed full-instruction maxima are 390,628/390,786 CU, with a 108,512-byte + isolated linked delta. + +- Added constant-time continuous arithmetic-Asian / TWAP option pricing with + future-starting windows and partially fixed averages. The implementation + evaluates the first two GBM average moments at HP precision, uses a + cancellation-safe short-window series, exposes exact discounted put-call + parity, and labels the final lognormal moment match as an approximation. + Added `TwapInputs`, exactly 100,000 stratified production and 10,000 + adversarial 60-digit mpmath vectors, 10,000 separately generated QuantLib + 1.41 vectors, examples, documentation, and an Anchor/SBF composite path. The + full corpus exposed catastrophic cancellation at raw carry seams; a stable + second-order small-carry expansion fixed it. Production then exposed a gap in + the adversarial design: near-ATM, heavily fixed contracts with very little + residual variance. That regime is now retained explicitly. All 110K compiled + accuracy calls succeed, with production/adversarial price maxima of + 22,580/19,587,949 raw. + Reusing the certified SCALE log/CDF kernels reduced deployed compute: the 2K + practical sweep measured 137,997 average / 180,029 P99 / 182,458 max math + CU, while the 10K adversarial sweep maxed at 185,590 math / 186,610 complete- + instruction CU, both below 200K. + +- Replaced the standard `ln_fixed_i` 16-anchor/Remez path with the shared + 1,024-segment Q42 midpoint kernel. The full 100K production and 10K + full-width adversarial corpora now measure 2 ULP max, 1 ULP P99, and zero + median error. Narrowing the exact LUT index from `u128` to `u64` removed a + wide-division helper. The final exp-linked composite measured 705 average / + 808 maximum CU; the earlier isolated ln harness measured 712 / 813. +- Refit `norm_cdf_poly` as ten half-sigma guarded body polynomials and four + direct tail polynomials with a balanced dispatch tree. Standalone CDF now + performs no exponential or division, has no answer lookup table, measures + 2 ULP max on both 100K/10K corpora, and measured 960 average / 993 maximum + CU on the final deployed composite artifact. +- Replaced `exp_fixed_i`'s division-heavy full-ln(2) rational with a + calculation-first, division-free ln(2)/32 kernel: five Q22 minimax Horner + products, a split-i64 Q63 residual, and 32 rounded Q62 fractional power-of-two + reconstruction constants (304 bytes, no sampled-answer table). Direct SBF + compute fell from 6,027 average / 6,365 max to 961 / 992 CU, while the + isolated linked path became 21,272 bytes smaller. Production max/P99/median + improved from 449,129,270 / 121,816,490 / 1 raw units to + 33,622 / 7,881 / 0; a regenerated 10K corpus brackets every new reduction + seam and retains the amplified positive-tail worst zone, measuring + 15,727,361,334,177 max / 6,704,999,717,817 P99 / zero median raw error. +- Reran every retained exp-dependent reference campaign. PDF exactness rose + from 23.778% to 42.560%; deterministic-Heston call/put maxima improved from + 245/486 to 200/372; BVN, Phi2, SABR, and fail-closed contracts held. A few + composed power/BS maxima increased because the former exp error had + accidentally cancelled upstream approximation error. Production IV accepts + 64 fewer rows due fixed-iteration verification-threshold sensitivity even + though the new discount is correctly rounded on every investigated lost row; + this is disclosed in the exp downstream report. +- Added reproducible, source-bound Arb/exact-integer release certificates for + the new logarithm, exponential, and normal-CDF kernels. The logarithm + certificate proves less than 2.925564 raw real error and at most 3 integer + ULP for every valid `u128` input. The exponential certificate proves relative + error below 1.55×10^-16, raw error below 41,159 over `|x| < 20`, and + monotonicity across every raw input; its full `(-40,40)` raw bound is + 2.21×10^13 because absolute error scales with the result. The CDF certificate + proves less than 2.393619 raw real error, at most 2 integer ULP, exact + symmetry, and nondecreasing output for every `i128`. + Its exact discrete analysis found two real one-ULP inversions missed by dense + sweeps; Q23 body coefficients and a payload-free Q39 tail evaluation guard + remove both while retaining the 936-byte coefficient/cutoff payload. +- Reran 2,719,550 downstream output checks across power, inverse CDF, fused + CDF/PDF, standard Black-Scholes/Greeks, IV, and SABR ratio paths. Added an + explicit near-one/large-exponent HP fallback after the improved log exposed + a composition-sensitive power regression. +- Closed the remaining reference gaps with fresh 22,500-case BVN, 20,000-case + Phi2 off-grid, 100,000-case full SABR, 200,704-case deterministic-Heston, + 100,000-case stochastic-Heston rejection, and public NIG rejection + campaigns. GL20 BVN slightly improved; SABR price's maximum tail error moved + from 879 to 1,130 raw units while P99 and median stayed at 126 and zero. +- Added a locked composite SBF harness and remeasured every `ln`/CDF-affected + executable path in a 55,400-call, zero-harness-error deployed campaign, + including power, inverse + CDF, fused CDF/PDF, Black-Scholes and every Greek, IV, SABR, BVN, Heston, + NIG rejection, and Phi2 lookup. Average CU fell 51.9% for standard BS price, + 36.8% for `bs_full`, 35.5% for IV, and 27.3% for guarded SABR price. + +- Added `ln_1p_fixed` for signed, cancellation-safe `ln(1+x)`: a dedicated + division-free Q42/table kernel measured at 2 raw-unit production maximum, + zero median error, and 542 average / 811 production-maximum CU over the + retained native and deployed-SBF campaigns. +- Replaced `expm1_fixed`'s degree-11 Taylor/general-exp paths with a + division-free raw-Q22/Q43 midpoint kernel: ordinary-domain error improved + from 11 to 3 raw units, while deployed SBF compute fell to 783 average / + 1,041 production maximum CU. +- Shared the rounded `k·ln(2)` reduction table between `ln_1p_fixed` and + `expm1_fixed`, reducing their combined raw LUT payload from 29,800 to 27,944 + bytes. Added compile-time table caps and a locked SBF footprint harness that + guards the linked binary deltas against regression. +- Fixed signed/unsigned half rounding, maximum-input square root, DoubleWord + tie/overflow behavior, power exponent conversion, large-angle reduction, + complex wide arithmetic, BS parity/bounds, IV preflight, and token/pool + rounding/domain defects. +- Added persisted-breach barrier pricing and protocol-favouring weighted-pool + execution inside a certified ratio/weight domain. +- Made positive-expiry stochastic Heston and NIG public pricing fail closed; + added a cancellation-safe exact deterministic-Heston reduction. +- Added SABR executable-domain/arbitrage guards and near-singular BVN + fail-closed behavior; replaced overshooting Phi2 cubic interpolation with + monotone bilinear interpolation and disclosed its measured precision. +- Hardened reference scripts, dependencies, CI permissions/action pins, + overflow-profile testing, and release guidance. +- Reran final native accuracy corpora and deployed-SBF CU distributions for + every changed executable path; evidence is under `.superstack/`. + ## 0.1.5 - 2026-04-24 - Added `VALIDATION.md` with release commands, package checks, reference asset diff --git a/Cargo.toml b/Cargo.toml index 663cfab..ae35282 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,12 @@ [package] name = "solmath" -version = "0.1.5" +version = "0.2.0" edition = "2021" rust-version = "1.79" -description = "Fixed-point financial math for Solana. Black-Scholes, Greeks, IV, NIG pricing, pool math — pure integer arithmetic, no_std, zero dependencies." +description = "Deterministic fixed-point math and quantitative finance for Solana: Greeks, IV, American KBI, NIG, TWAP, and DeFi primitives." license = "MIT OR Apache-2.0" repository = "https://github.com/DJBarker87/solmath" +documentation = "https://docs.rs/solmath" keywords = ["solana", "fixed-point", "black-scholes", "defi", "math"] categories = ["mathematics", "no-std::no-alloc"] readme = "README.md" @@ -13,24 +14,29 @@ include = [ "Cargo.toml", "README.md", "INTEGRATION.md", - "SECURITY.md", "USAGE.md", - "PROOFS.md", + "SECURITY.md", "VALIDATION.md", "CHANGELOG.md", "LICENSE-APACHE", "LICENSE-MIT", - "docs/**", - "examples/**", - "benchmark/iv_vectors.json", - "test_data/heston_reference_tests.rs", - "test_data/sabr_reference_tests.rs", + "docs/ARCHITECTURE.md", + "docs/AMERICAN_KBI.md", + "docs/ASIAN_TWAP.md", + "docs/NIG.md", + "examples/*.rs", + "!examples/README.md", "src/**", ] [features] -default = ["transcendental", "complex"] -full = ["transcendental", "complex", "bs", "iv", "barrier", "nig", "heston", "sabr", "pool", "bivariate"] +# Keep the default useful for general fixed-point clients without linking any +# pricing model or the niche complex-arithmetic surface into downstream SBF +# programs. Every model remains an explicit capability. +default = ["transcendental"] +# Stable runtime capabilities only. Offline table generation and the alternate +# Padé IV path intentionally remain explicit opt-ins. +full = ["transcendental", "complex", "bs", "iv", "barrier", "asian", "nig", "heston", "sabr", "pool", "bivariate", "american-kbi", "rainbow"] transcendental = [] bivariate = ["transcendental"] table-gen = ["bivariate"] @@ -38,16 +44,39 @@ complex = ["transcendental"] bs = ["transcendental"] iv = ["bs"] barrier = ["transcendental"] -nig = ["transcendental", "complex"] -heston = ["bs", "complex"] +# Continuous arithmetic-Asian / partially fixed TWAP settlement pricing. +asian = ["transcendental"] +# Kim Boundary Integration: a nonlinear smooth-pasting exercise boundary plus +# Kim's early-exercise-premium integral, evaluated entirely in the program. +american-kbi = ["transcendental"] +# Two-asset rainbow options (worst-of / best-of) via bivariate normal CDF. +rainbow = ["bivariate"] +# Exponential NIG pricing uses the crate's fixed-point exp/log kernels. +nig = ["transcendental"] +heston = ["bs"] sabr = ["transcendental"] pool = ["transcendental"] pade-iv = ["iv"] -idl-build = [] [dependencies] # Zero dependencies — pure Rust, no_std +[package.metadata.docs.rs] +all-features = true +targets = ["x86_64-unknown-linux-gnu"] + +[lints.rust] +# Kani injects its proof API only while `cargo kani` is compiling the crate. +# Declaring the cfg keeps modern rustc check-cfg output actionable without +# adding a runtime or development dependency. +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(kani)'] } + +# Defense in depth for this repository's own release/SBF builds. Cargo ignores +# dependency profile settings, so downstream Solana programs must set this too +# in their workspace root (see INTEGRATION.md). +[profile.release] +overflow-checks = true + [[example]] name = "options_pricing" path = "examples/options_pricing.rs" @@ -58,7 +87,27 @@ name = "weighted_pool_swap" path = "examples/weighted_pool_swap.rs" required-features = ["pool"] +[[example]] +name = "nig_batch" +path = "examples/nig_batch.rs" +required-features = ["nig"] + [[example]] name = "safe_token_conversion" path = "examples/safe_token_conversion.rs" required-features = ["pool"] + +[[example]] +name = "american_kbi_batch" +path = "examples/american_kbi_batch.rs" +required-features = ["american-kbi"] + +[[example]] +name = "twap_options" +path = "examples/twap_options.rs" +required-features = ["asian"] + +[[example]] +name = "asian_batch" +path = "examples/asian_batch.rs" +required-features = ["asian"] diff --git a/INTEGRATION.md b/INTEGRATION.md index 3ecda33..287bc6e 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -8,21 +8,29 @@ receive already-validated integers scaled by `SCALE = 1_000_000_000_000`. ```toml [dependencies] -anchor-lang = "0.31" -solmath = { version = "0.1", default-features = false, features = ["transcendental"] } +anchor-lang = "0.32" +solmath = { version = "0.2", default-features = false, features = ["transcendental"] } ``` For pool math and token conversion, use: ```toml [dependencies] -anchor-lang = "0.31" -solmath = { version = "0.1", default-features = false, features = ["pool"] } +anchor-lang = "0.32" +solmath = { version = "0.2", default-features = false, features = ["pool"] } +``` + +American KBI and exponential NIG can be linked independently: + +```toml +solmath = { version = "0.2", default-features = false, features = ["american-kbi"] } +# or: features = ["nig"] ``` ## Shared Error Mapping -Use one mapper at your program boundary. Do not unwrap math results. +Use one mapper at the program boundary so every numerical outcome becomes a +stable program error. ```rust use anchor_lang::prelude::*; @@ -52,10 +60,72 @@ pub fn map_math_error(err: SolMathError) -> anchor_lang::error::Error { } ``` +## Validate Inputs Once (Recommended) + +The raw pricing functions accept integer inputs and return domain/arithmetic +errors through `Result`. The +`checked` module goes further: it makes the valid financial domain a **type**. +Validate untrusted instruction data once at your program boundary into +`Price` / `Rate` / `Vol` / `Time`, bundle them, and every downstream pricing +call is guaranteed not to panic or silently wrap—the internal bounds the +kernels assume are established by construction. A degenerate-but-in-range input +returns its normal `Err` variant without aborting the process. + +```rust +use solmath::{EuropeanInputs, ImpliedVolInputs}; + +// At the boundary: one fallible validation of raw instruction data. +let quote = EuropeanInputs::from_raw(s, k, r, sigma, t).map_err(map_math_error)?; +let greeks = quote.full().map_err(map_math_error)?; // cannot panic +let call = greeks.call; + +// Implied volatility from an observed price, same pattern. +let iv = ImpliedVolInputs::from_raw(market_price, s, k, r, t) + .map_err(map_math_error)? + .solve() + .map_err(map_math_error)?; +``` + +The same pattern covers the other value-bearing entry points: + +```rust +use solmath::{BarrierInputs, PoolSwapInputs, TwapInputs}; +use solmath::barrier::BarrierType; + +let knock = BarrierInputs::from_raw(s, k, h, r, sigma, t).map_err(map_math_error)?; +let out = knock.price(true, BarrierType::DownAndOut).map_err(map_math_error)?; // cannot panic + +// TWAP validity is relational too: averaging_time <= t, and the fixed +// average/weight must describe one coherent observation state. +let twap = TwapInputs::from_raw( + s, k, r, q, sigma, t, averaging_time, fixed_average, fixed_weight, +) +.map_err(map_math_error)? +.price() +.map_err(map_math_error)?; + +// Pool validity is relational (weight ratio ≤ 20, post-trade balance ratio ≥ 0.01), +// so from_raw checks the whole certified domain at once. +let (net_out, fee) = PoolSwapInputs::from_raw( + balance_in, balance_out, weight_in, weight_out, amount_in, fee_rate, +) +.map_err(map_math_error)? +.quote() +.map_err(map_math_error)?; +``` + +Certified domain (generous — rescale larger economics homogeneously): +`Price ≤ 100,000`, `Rate ≤ 1000%`, `0 < Vol ≤ 10000%`, `0 < Time ≤ 100` years; +pool swaps use the kernel's relational domain. The no-panic guarantee over the +whole domain is verified continuously by the `tests/checked_layer.rs` sweep +(European, IV, barrier, TWAP, and pool), which CI runs in a scaled soak with overflow +checks on. Prefer this layer for value-bearing paths; drop to the raw functions +only when you need an input outside the certified box and have your own bound. + ## 1. Options Quote -Use `bs_full_hp` when accuracy matters. Current benchmark: ~118K CU average, -~165K max for price + all 5 Greeks. A quote-only instruction fits under the +Use `bs_full_hp` when accuracy matters. Final-artifact benchmark: 113,177 CU +average and 149,925 max for price + all 5 Greeks. A quote-only instruction fits under the default 200K CU budget; request 250K+ if the same instruction also settles, writes multiple accounts, or performs CPIs. @@ -130,15 +200,75 @@ pub fn quote_option( } ``` +### American quote with KBI + +For an American settlement mark, enable `american-kbi` and pass only the six +scaled market/contract inputs. The boundary and Kim early-exercise-premium +integral are evaluated in the instruction; there is no operator account or +off-chain quote payload. + +```rust +use solmath::{american_kbi_price, AmericanKbiKind}; + +let price = american_kbi_price( + spot, + strike, + risk_free_rate, + dividend_yield, + volatility, + years_to_expiry, + AmericanKbiKind::Put, +) +.map_err(map_math_error)?; +``` + +The retained full-instruction maxima are 390,628 CU for calls and 390,786 CU +for puts. A 400K limit covers the measured quote-only instruction; add measured +headroom for account writes, oracle reads, or CPIs. The parameter domain and +QuantLib QdFp evidence are in `docs/AMERICAN_KBI.md`. + +### Exponential NIG quote + +Enable `nig` to return the European call/put pair, quote-local numerical +allowance, and selected execution tier in one call: + +```rust +use solmath::{nig_price_certified, NigParams, SCALE}; + +let quote = nig_price_certified( + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, // rate = 5% + 20_000_000_000, // dividend yield = 2% + SCALE, + NigParams { + alpha: 15 * SCALE, + beta: -2 * SCALE as i128, + delta_per_year: SCALE, + }, + 5_000_000_000, // requested absolute error = 0.005 +) +.map_err(map_math_error)?; +``` + +`quote.tier` distinguishes expiry, the inexpensive Chernoff tail, and the full +15-node quadrature path. The current deployed maximum is 382,441 CU; the full +domain and accuracy evidence are in `docs/NIG.md`. + ## 2. Weighted Pool Swap Quote Use `weighted_pool_swap` for Balancer-style weighted pools. It returns `(net_out, fee)` at `SCALE`. Convert raw token amounts to fixed-point before calling it, and convert the output back to raw token units with floor rounding. +The safe power-error proof requires the post-trade balance ratio to be at least +1% and `weight_in / weight_out <= 20`; unsupported shapes return +`DomainError`. Compute budget: the swap path uses one HP power calculation. Start with the default 200K CU budget for quote-only instructions, and benchmark your full instruction if you add token CPIs or multi-hop routing. +The final math-only sweep over 900 certified-domain cases averaged 25,608 CU +and maxed at 33,685 CU. Client-side budget: @@ -262,9 +392,38 @@ pub fn settle_amounts( | Use case | Suggested limit | Notes | |----------|-----------------|-------| -| `bs_full_hp` quote | 200K quote-only, 250K+ with extra logic | Benchmarked ~118K avg / ~165K max. | +| `bs_full_hp` quote | 200K quote-only, 250K+ with extra logic | Benchmarked 113,177 avg / 149,925 max. | +| `american_kbi_price` | 400K quote-only; more with account/CPI work | Fully on-chain boundary and premium integration. Full-instruction call/put maxima 390,628/390,786 CU on 2,000 quotes per leg. | +| `nig_price_certified` | 400K quote-only | 2K deployed sweep: 367,321 P99 / 381,385 max math CU and 382,441 max full-instruction CU. Deep Chernoff-tail quotes are much cheaper; domain/error-budget misses return `SolMathError`. | | Weighted pool quote | 200K quote-only | Benchmark if combined with token CPIs or routing. | | Token conversion | Default budget | Helpers are simple integer conversions. | +| `exp_fixed_i` | Default budget | 961 average / 992 max CU. Certified relative error is <1.55×10^-16; absolute raw error grows with the result. | +| `barrier_option_with_state` | 500K minimum | Math-only max 415,579 CU. Persist historical breach state. | +| `twap_option_price` | Fits the default 200K benchmark instruction | 100K production + 10K adversarial accuracy calls complete without rejection; deviations are reported in `benchmark/asian_accuracy_report.json`. 2K practical CU calls: 137,997 average / 180,029 P99 / 182,458 math max. The 10K adversarial CU sweep maxed at 185,590 math / 186,610 complete-instruction CU. Persist authenticated fixing state; extra oracle/account/CPI work needs added budget. | +| `implied_vol` | 500K minimum | Fresh math-only P99 282,132 / max 328,660 CU; 17/2,000 sampled inputs returned an expected error. | +| deterministic `heston_price` (`xi = 0`) | 250K minimum | Fresh 2K math-only max 183,239 CU; the broader 2,004-case grid maxed at 190,756. The API returns `NoConvergence` for `xi > 0`. | +| guarded `sabr_price` / `sabr_greeks` | 700K minimum | Fresh accepted-case math-only maxima 603,172 / 603,160 CU; rejected shapes return an error. | +| `bvn_cdf` / `bvn_cdf_hp` | 250K / 550K minimum | Conservative broader branch-grid maxima remain 208,693 / 468,417 CU; the fresh 2K exp-affected samples measured 100,498 / 248,294 average and 135,944 / 307,310 max. | +| `Phi2Table::eval` | Default budget | Runtime-backed math-only max 1,440 CU. | + +These are SolMath-call measurements, not complete instruction budgets. Add +headroom for Anchor dispatch, account serialization, logs, oracle work, and +CPIs, and gate regressions against your deployed artifact. NIG exposes its +documented domain and requested-error contract through `DomainError` and +`NoConvergence`. `heston_price` implements the deterministic positive-expiry +`xi == 0` limit. + +## Required release profile + +SolMath returns arithmetic failures as `SolMathError`, but an on-chain +consumer should still keep Rust overflow checks enabled as defense in depth. +Add this to the **workspace-root** `Cargo.toml` (a dependency cannot impose +the setting on its caller): + +```toml +[profile.release] +overflow-checks = true +``` -For higher-cost models such as `implied_vol`, `barrier_option`, and `nig_64`, -request a larger budget in the client before calling the instruction. +CI should test release builds both with checks enabled and disabled; SolMath's +own workflow does this to catch profile-dependent behavior. diff --git a/PROOFS.md b/PROOFS.md index 6396da3..e71266d 100644 --- a/PROOFS.md +++ b/PROOFS.md @@ -7,18 +7,22 @@ least-significant fixed-point unit. All source references are to files under `src/`. -**Disclaimer:** The methodology and assumptions in this document are sound to the -author's knowledge, but the proofs themselves were generated with AI assistance and -have not been independently verified. The discerning reader should reproduce any -result before relying on it. The empirical benchmarks were run by the author and are -the primary accuracy reference. - -**Certificate script status:** The Lipschitz certificate scripts referenced below -(`lipschitz_certificate.py`, `trig_lipschitz_certificate.py`) are not included in -the current crate package. The polynomial certification chain for Propositions 6, -9, and 10 should be treated as non-reproducible until those scripts are restored -and re-run against the current source. The empirical benchmark results remain the -primary accuracy reference. +**Verification status.** The crate uses four distinct evidence layers and labels +their scope explicitly: + +1. Bit-precise Kani proofs for core truncation, nearest-rounding, overflow, + double-word residuals, U256 limb arithmetic, and square-root bisection. +2. Source-bound Arb interval certificates for the current `ln_fixed_i`, + `exp_fixed_i`, and `norm_cdf_poly` kernels. +3. Exact-integer and analytical arguments for arithmetic not yet covered by a + model-checking harness. +4. Independent-reference corpora for end-to-end model accuracy. + +The current Kani harnesses and Arb entry points are reproducible from this +repository and enforced by CI. Historical sections that refer to the retired +`lipschitz_certificate.py` or `trig_lipschitz_certificate.py` scripts do not form +part of the current source-bound certificate set; this primarily affects the old +trigonometric and HP-CDF derivations, not the current standard CDF certificate. ----- @@ -102,6 +106,61 @@ division semantics for R3; standard library `i128::checked_mul` documentation fo ----- +## Machine-checked integer core + +Kani 0.67.0 model-checks thirteen production-linked integer properties over their +complete stated state spaces: 545 checks in the current run. The proof code +lives beside the implementation under +`#[cfg(kani)]`, so it contributes nothing to the published runtime or program +binary. CI runs all harnesses with: + +```sh +cargo kani --features full +``` + +The harnesses are: + +| Harness | Bit-precise result | Production scope | +|---|---|---| +| `signed_scale_remainder_is_sub_ulp` | For every `i128` numerator, `abs(n % SCALE) < SCALE`; truncation therefore contributes strictly less than 1 ULP. | Standard-scale signed multiplication/division lemmas. | +| `remainder_half_comparison_is_exact_and_overflow_free` | `r >= d-r` is exactly equivalent to `2r >= d`, including odd divisors, exact ties, and cases where `2r` overflows `u128`. | Every nearest-rounding helper. | +| `unsigned_quotient_rounding_is_half_ulp_or_overflow` | For every valid `(q,r,d)`, nearest rounding has error at most 0.5 ULP, ties round up, and `MAX+1` returns overflow. | `fp_mul_round`, `fp_div_round`, `fp_div_i_round`, and the widened branch of `fp_mul_i_round`. | +| `signed_quotient_rounding_is_half_ulp_or_overflow` | For every valid signed quotient/remainder, nearest rounding has error at most 0.5 ULP, ties round away from zero, and unrepresentable corrections return overflow. | Native-product branch of `fp_mul_i_round`. | +| `standard_collapse_is_half_ulp_for_every_valid_residual` | Every valid standard-scale `DoubleWord` residual collapses with at most 0.5 ULP error. | Compensated standard-scale kernels. | +| `hp_collapse_is_half_ulp_for_every_valid_residual` | The same result holds at `SCALE_HP = 10^15`. | Compensated HP kernels. | +| `checked_add_preserves_the_sub_ulp_residual_invariant` | Exact double-word addition re-normalizes every accepted residual into `(-SCALE, SCALE)`. | Composition of compensated operations before final collapse. | +| `u256_product_carry_assembly_is_exact` | For every valid tuple of four 64×64 partial products, all four base-2^64 result limbs equal an independent schoolbook-column specification. | `U256::mul_u128` and the shared `wide_mul_u128` square-root comparator. | +| `u256_div_rem_u64_digit_fits_one_limb` | For every proper incoming remainder, the appended two-limb dividend is strictly below `divisor * 2^64`; its quotient digit therefore fits one limb. | Every transition of `U256::div_rem_u64`. | +| `knuth_d3_two_corrections_cross_the_base` | For every normalized top divisor limb, two correction transitions necessarily raise `rhat` across base 2^64 without overflow; the D3 loop needs at most two decrements. | Quotient-digit refinement in `U256::div_rem_u128`. | +| `sqrt_newton_transition_preserves_and_detects_the_floor` | Given the exact quotient inequalities implied by a floor-root bracket, production Newton averaging preserves `candidate >= floor_root`, strictly descends above the floor, and can exit only at the floor. | `isqrt_u128` and the fallback behind fixed-iteration `sqrt_scaled_newton`. | +| `sqrt_bisection_preserves_the_exact_floor_bracket` | For every valid bracket and midpoint decision, the production transition preserves `low <= floor_root < high` and strictly shrinks the bracket. | Wide bisection in the overflow path of `fp_sqrt`. | + +For an exact quotient/remainder pair `x = q + r/d`, the nearest-rounding +harnesses define the integer error numerator as either `abs(r)` or +`d - abs(r)`. They prove the overflow-free inequality + +```text +error_numerator <= d - error_numerator +``` + +which is equivalent to `2 * error_numerator <= d`, hence an error of at most +one-half output ULP. This avoids floating-point arithmetic and avoids overflowing +the proof expression itself. + +The rounding harnesses deliberately start at the exact quotient/remainder +boundary. The widened-arithmetic proof is compositional: **(R1)** supplies exact +64×64 partial products, the 79-check carry harness proves their 256-bit +assembly, **(R2)**/**(R3)** supply each primitive quotient/remainder identity, +and the division harnesses prove the nontrivial limb-fit and Knuth-D3 bounds. +Lemma 3 gives the induction over all limbs. For square root, the 31-check Newton +harness proves the production control transition from exact quotient +inequalities, while exact multiplication makes `midpoint² <= n` equivalent to +`midpoint <= floor(sqrt(n))` and the 14-check harness proves the wide-bisection +transition. Polynomial approximation error is separate and is covered by +source-bound Arb certificates. + +----- + ## Lipschitz certificate method Several Category B results (Proposition 6, Proposition 9, Proposition 10) claim that @@ -168,22 +227,19 @@ and Proposition 10, and detailed further in the Appendix (Lipschitz certificates ## Bound classification -Not all bounds in this document have the same epistemological status. Each result -falls into one of three categories: +Not all bounds in this document use the same proof mechanism: -|Category |Meaning |Results | -|----------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -|**A — Exact integer proofs** |The computation is proven exact (0 error) or the only error is the unavoidable final truncation/rounding remainder, proved from the code's integer arithmetic with no approximation theory involved. |Lemma 1, Proposition 1 Path B, Lemma 3, Lemma 2, Proposition 2, Proposition 3, Proposition 4, Proposition 5 | -|**A/B — Exact with analytical convergence argument**|The computation is structurally exact (integer Newton iteration converging to floor(sqrt)), with convergence guaranteed by a standard analytical argument. No approximation theory involved, but the convergence proof is not machine-checked. |Proposition 1 Path A | -|**B — Analytical conservative bounds** |The error bound is derived by summing worst-case contributions from each rounding step in the algorithm. For polynomial approximation results, the minimax approximation error is rigorously certified via Lipschitz analysis. These are genuine analytical arguments but rely on worst-case accumulation that is unlikely to be realized simultaneously.|Proposition 6, Proposition 7, Proposition 8, Proposition 9, Proposition 10, Proposition 11 (restricted domain), Proposition 12 | +| Category | Meaning | Results | +|---|---|---| +| **M — Machine checked** | Bit-precise verification of the compiled Rust control flow and integer properties for every state satisfying the harness preconditions. | The rounding, residual, U256-limb, and square-root transition harnesses listed above. | +| **A — Exact integer proof** | Exact computation, or only an unavoidable final integer remainder, established algebraically without approximation theory. | Lemmas 1–3; both paths of Proposition 1; Propositions 2–5. | +| **B — Certified or analytical bound** | Interval-certified approximation error and/or conservative accumulation of rounding contributions. | Propositions 6–12. | -**How to read this document:** Category A bounds are unconditionally reliable — they -follow from integer arithmetic identities that hold by construction. Category B bounds -are reliable but conservative; they could in principle be tightened by tracking error -correlations. For polynomial approximation results (Proposition 6, Proposition 9, -Proposition 10), the minimax approximation error is rigorously certified via Lipschitz -analysis (see Appendix for the three-level rigour chain). All bounds are consistent -with (and significantly more conservative than) empirical benchmarks. +Category M is the strongest implementation-level evidence but applies only to +the stated harness boundary. Category A supplies the exact arithmetic premises +and induction that compose those boundaries into whole-function results. +Category B covers approximation kernels and is normally more conservative than +the measured error. ----- @@ -375,18 +431,35 @@ result = trunc((a * b) / c) exactly (0 error). *(A) The 256-bit multiplication is exact.* Since `|a| <= 2^127` and `|b| <= 2^127` (both fit in i128), the product `|a| * |b| <= 2^254`, which fits in U256 (256 bits). The implementation -`U256::mul_u128` (`constants.rs:125-146`) performs schoolbook multiplication -on 64-bit limbs with carry propagation. Each limb multiplication is exact -by **(R1)** and carry propagation is exact integer addition. No truncation -or rounding occurs. +`U256::mul_u128` performs schoolbook multiplication on 64-bit limbs. Each +partial product fits in u128 and is exact by **(R1)**. Production carry +propagation is factored through `assemble_u128_product`; the Kani harness +`u256_product_carry_assembly_is_exact` checks 79 properties over every valid +four-partial-product state and proves all four limbs equal independent +base-2^64 schoolbook columns. `wide_mul_u128` delegates to this same production +multiplier, so the square-root comparator shares the result. No truncation or +rounding occurs. *(B) The 256-bit division is exact integer division.* -`div_rem_u64` (`constants.rs:165-178`) performs long division using 128-bit -intermediate values for each 64-bit limb. `div_rem_u128` (`constants.rs:181+`) -performs Knuth's Algorithm D (normalized long division). Both operate on unsigned -(absolute) values and compute `floor(numerator / divisor)` by **(R2)** and the -exact remainder by **(R3)**. For unsigned values, floor division and truncation -toward zero are identical. +Let `B = 2^64`. `div_rem_u64` applies four production +`div_rem_u64_step` transitions. If the incoming remainder satisfies `r < d`, +then the appended dividend `rB + limb < dB`; the Kani harness +`u256_div_rem_u64_digit_fits_one_limb` verifies that bound over every 64-bit +state. Hence the quotient digit is below `B`, while **(R2)**/**(R3)** give +`rB + limb = qd + r'` and `r' < d`. Induction from the initial remainder zero +proves the four-limb quotient and final remainder exactly. + +`div_rem_u128` performs normalized Knuth Algorithm D. Normalization makes the +top divisor limb at least `B/2`. The production D3 correction is factored into +`advance_knuth_refinement`; the 45-check +`knuth_d3_two_corrections_cross_the_base` harness proves that, if a second +correction is needed, it necessarily raises `rhat` across `B`, so the loop is +bounded by two decrements and its additions/subtractions remain representable. +The subsequent multiply-subtract and possible add-back are the standard exact +Algorithm-D limb transitions under **(R1)**–**(R3)**. In test builds every +optimized result is also asserted equal to the independent 256-step restoring +long-division reference in `overflow.rs`. Both division paths therefore compute +the unsigned floor quotient and exact proper remainder. *(C) Sign correction is exact.* Negation of an integer is exact in two's complement, with the one edge case @@ -429,10 +502,11 @@ Ordered so that each result depends only on lemmas and propositions already stat |fp_sqrt(x) - sqrt(x * SCALE)| < 1 ULP. ``` -**Classification.** Path B is Category A (exact by 256-bit bisection). Path A is -Category A/B: the algorithm is structurally exact (integer Newton iteration converging -to floor(sqrt)), with convergence guaranteed by a standard analytical argument that -is not machine-checked. Both paths are empirically validated to < 1 ULP on all tested inputs. +**Classification.** Both paths are Category A. Fixed-iteration Newton remains +the fast candidate generator, but a release feasibility certificate and an +exact monotonically convergent integer-Newton fallback make correctness +independent of the fixed iteration count. The Newton control transition and the +wide-bisection transition are machine-checked over their complete stated spaces. **Implementation.** Two code paths depending on whether `x * SCALE` fits in u128: @@ -446,6 +520,9 @@ is not machine-checked. Both paths are empirically validated to < 1 ULP on all t 1. Initial guess: `g = 1 << ((bit_len + 1) / 2)` where `bit_len = 128 - scaled.leading_zeros()` 1. Up to 8 Newton-Raphson iterations: `g_new = (g + scaled / g) / 2` 1. Early termination when `g_new == g` or `g_new + 1 == g`; returns `min(g, g_new)` +1. Verify `g² <= scaled`. Every Newton transition preserves + `g >= floor(sqrt(scaled))`, so this one-sided feasibility test proves equality. + If it does not hold, restart with exact convergent `isqrt_u128`. **Path B** (overflow, `arithmetic.rs:229-262`): When `x * SCALE` overflows u128: @@ -469,6 +546,8 @@ error, but this only affects the initial approximation for the bisection. `low = max { r : r^2 <= x * SCALE }`, where the comparison `r^2 <= x * SCALE` is performed in exact 256-bit arithmetic via `cmp_sqrt_candidate(mid, x)`, which computes `wide_mul(mid, mid)` vs `wide_mul(x, SCALE)` with no overflow. +`wide_mul_u128` delegates to the same `U256::mul_u128` carry assembly covered by +the 79-check multiplication harness. *(C) Termination.* The binary search terminates when `low + 1 == high`, which means `low^2 <= x*SCALE < (low+1)^2`. This is exactly the condition @@ -477,58 +556,49 @@ means `low^2 <= x*SCALE < (low+1)^2`. This is exactly the condition *(D) Error.* Path B returns `floor(sqrt(x * SCALE))` exactly. The error satisfies `sqrt(x * SCALE) - floor(sqrt(x * SCALE)) ∈ [0, 1)`. □ (Path B) -*Proof of Path A (Category A/B — analytical convergence).* +*Proof of Path A (Category A — certified exact fallback).* *(A) Scaling.* The product `scaled = x * SCALE` is exact by **(R1)**, since the checked multiply confirms no overflow. -*(B) Overestimate.* The initial guess is `g_0 = 2^(ceil(bit_len/2))`. Since -`2^(ceil(log2(n)/2)) >= sqrt(n)` for all n > 0, the initial guess satisfies -`g_0 >= sqrt(scaled)`. - -*(C) Monotone convergence.* Each iteration computes -`g_{k+1} = floor((g_k + scaled/g_k) / 2)` by **(R2)**. For any g > floor(sqrt(n)), -the integer iteration `floor((g + floor(n/g)) / 2)` is strictly less than g — this -is the key property that makes integer Newton iteration terminate. The floor operations -in integer division do not break monotone convergence; they ensure it. The sequence is -non-increasing for k >= 1 and converges to floor(sqrt(scaled)). (This is a standard -result; see Cohen, *A Course in Computational Algebraic Number Theory*, §1.7.1.) +*(B) Newton lower-bound invariant.* Let `s = floor(sqrt(scaled))` and let the +current positive candidate be `g >= s`. Since `scaled >= s²`, -*(D) Convergence rate.* Let `m = bit_len` so that `2^(m-1) <= scaled < 2^m`. -The true root satisfies `sqrt(scaled) >= 2^((m-1)/2)`. Therefore: - -``` -g_0 / sqrt(scaled) <= 2^(ceil(m/2)) / 2^((m-1)/2) = 2^(ceil(m/2) - (m-1)/2) +```text +g + floor(scaled / g) >= 2s, ``` -For even m: `ceil(m/2) - (m-1)/2 = 1/2`, so `g_0/sqrt(scaled) <= sqrt(2) < 2`. -For odd m: `ceil(m/2) - (m-1)/2 = 1`, so `g_0/sqrt(scaled) <= 2`. -In both cases, `g_0 / sqrt(scaled) <= 2`. The first iteration yields: - -``` -g_1/sqrt(scaled) - 1 <= (g_0/sqrt(scaled) - 1)^2 / (2 * g_0/sqrt(scaled)) <= 1/4 -``` +so `floor((g + floor(scaled/g))/2) >= s`. Thus every production averaging +transition and the early `min(g, g_new)` preserve `g >= s`. The 31-check harness +`sqrt_newton_transition_preserves_and_detects_the_floor` machine-checks the +averaging, preservation, and exit control over every state satisfying these +exact quotient inequalities. -so `g_1` is within 25% of the root and quadratic convergence applies from k=1 -onwards. After 8 iterations, the relative error is bounded by -`(0.25)^(2^7) = (0.25)^128 < 10^-77` — far below a single ULP for any 128-bit input. +*(C) One-sided release certificate.* After the fixed steps, the implementation +checks `g² <= scaled` with `checked_mul`. The invariant gives `g >= s`, while +feasibility gives `g <= s`; hence `g = s`. A second successor square is +unnecessary on the release hot path. Debug builds retain the full two-sided +certificate. -*(E) Termination.* The early termination condition (`g_new == g` or `g_new + 1 == g`) -detects convergence and returns `min(g, g_new) = floor(sqrt(scaled))`. +*(D) Exact fallback.* If feasibility rejects, `isqrt_u128` restarts from +`g_0 = 2^ceil(bit_len/2) >= s`. When `g > s`, +`scaled < (s+1)² <= g²`, so `floor(scaled/g) < g` and the next candidate is +strictly smaller than `g`, while part (B) keeps it at least `s`. A strictly +decreasing nonnegative integer sequence terminates. The production exit test is +`g_new >= g`; the machine-checked control lemma proves that condition cannot +hold above `s`, so the returned candidate is exactly `s`. -*(F) Error.* Path A returns `floor(sqrt(x * SCALE))` exactly. The error satisfies +*(E) Error.* Path A returns `floor(sqrt(x * SCALE))` exactly. The error satisfies `sqrt(x * SCALE) - floor(sqrt(x * SCALE)) ∈ [0, 1)`. □ (Path A) **Combined.** In both paths, `f-hat(x) = floor(sqrt(x * SCALE))` and the error `|f-hat(x) - f(x)| = sqrt(x*SCALE) - floor(sqrt(x*SCALE))` lies in [0, 1). Therefore **|f-hat(x) - f(x)| < 1 ULP** for all valid x. □ -*Remark (Post-check).* A `debug_assert!` post-check already exists at -`arithmetic.rs:186-191`, verifying `g * g <= scaled && (g+1) * (g+1) > scaled`. -This catches convergence failures during development and testing. Promoting it to -an unconditional `assert!` would upgrade Path A to Category A (the output would be -verifiably correct regardless of convergence analysis), at the cost of a small CU -increase in production. +*Remark (Fast-path cost).* One checked square executes in release builds. The +fallback reuses the crate's existing integer-Newton kernel and runs only if the +fixed candidate is infeasible; the proof upgrade does not add a second dense +square-root engine. *Remark (Empirical validation).* Max observed error = 1 ULP across benchmark vectors (production suite: fp_sqrt max ULP = 1 across 100K vectors). The ≤ 1 ULP @@ -677,18 +747,49 @@ and floor = ceil. □ ### Proposition 6 (norm_cdf_poly — standard-scale normal CDF) -**Source:** `normal.rs:80-117` +**Source:** `normal.rs`, `norm_cdf_coeffs.rs` -**Classification:** Category B — Lipschitz-certified approximation error plus -analytical implementation overhead. +**Classification:** Category B — rigorous Arb interval certificate plus exact +integer/rational analysis of every implementation case. -**Statement.** For all x in [-8*SCALE, 8*SCALE], +**Current implementation (2026-07-12).** Ten guarded half-sigma body +polynomials (degree 8/7) and four direct tail polynomials (degrees 6/5/4/3) +use Q44 arguments and a balanced dispatch tree. Coefficients are stored at Q23; +the body evaluates at Q23 and the tail promotes the same stored coefficients to +Q39 in a proved-safe `i64` accumulator. The standalone CDF performs no +exponential or division and retains a 936-byte coefficient/cutoff payload. +**All-input theorem.** For every `x: i128`, let +`y = SCALE * Phi(x / SCALE)` and let `R(y)` be a nearest integer. The checker +`scripts/certify_norm_cdf.py`, using `python-flint==0.8.0`/Arb at 256-bit +working precision, proves + +```text +|norm_cdf_poly(x) - y| < 2.393618011670762 raw units +|norm_cdf_poly(x) - R(y)| <= 2 integer ULP. ``` -|norm_cdf_poly(x) - Phi(x/SCALE) * SCALE| <= 9 ULP, -``` -where Φ is the standard normal CDF. +It also proves exact symmetry wherever negation is representable, safe handling +of `i128::MIN`, and nondecreasing output over the complete discrete `i128` +domain. All fourteen exact quantized polynomials have a strict derivative sign; +their minimum adjacent-input increment exceeds the complete rounded-Horner +uncertainty. Sixteen center/seam/cutoff transitions are checked exactly, and +Arb proves the one-to-zero raw tail transition. Every map multiply, body `i64` +accumulator, Q39 tail `i64` accumulator, `i128` product, half-rounding addition, +clamp, and symmetry subtraction is width-checked. + +Constructing the proof found two genuine one-ULP monotonicity reversals that +the former 110,000 vectors and dense sweep missed: one in the Q20 upper body and +one in the initial tail after the Q23 body repair. The final Q23-body/Q39-tail +kernel removes both structurally and retains exact compiled regressions. + +**Retained empirical result.** The final 100,000-vector production and 10,000- +vector seam/tail adversarial corpora still measure 2 ULP maximum. The complete +certificate, per-piece bounds, source digests, discrete proof, and limitations +are in `.superstack/norm-cdf-proof-2026-07-12.md` and its JSON companion. + +
+Historical six-piece/continued-fraction proof (superseded; not applicable to the current code) **Certificate summary** (`lipschitz_certificate.py`, 100K grid points/piece, mpmath 60-digit precision): @@ -816,22 +917,50 @@ are negligible relative to 1 ULP. The clamp to [0, SCALE_I] ensures safety. *Remark (Empirical validation).* Max observed error = 4 ULP across 100K production vectors (production suite: norm_cdf_poly max ULP = 4). +
+ ----- ### Proposition 7 (ln_fixed_i — fixed-point natural logarithm) -**Source:** `transcendental.rs:11-87` +**Source:** `transcendental.rs`, `ln_lut.rs`, `ln2_lut.rs` -**Classification:** Category B — analytical conservative bound with sub-ULP -correction chain. +**Classification:** Category B — rigorous Arb interval certificate plus exact +integer/rational analysis of every implementation case. -**Statement.** For all x > 0 where the result fits in i128, +**Current implementation (2026-07-12).** `ln_fixed_i` and `ln_1p_fixed` share +a 1,024-segment Q42 midpoint kernel. Binary normalization maps to `[1,2)`, an +exact 64-bit index selects a midpoint/log/reciprocal, and +`q - q²/2 + q³/3` supplies the local correction. +**All-input theorem.** For every `x` in `1..=u128::MAX`, let +`y = SCALE * ln(x / SCALE)` and let `R(y)` be a nearest integer. The checker +`scripts/certify_ln_fixed.py`, using `python-flint==0.8.0`/Arb at 100 decimal +digits, proves + +```text +|ln_fixed_i(x) - y| < 2.925564 raw units +|ln_fixed_i(x) - R(y)| <= 3 integer ULP. ``` -|ln_fixed_i(x) - ln(x / SCALE) * SCALE| <= 3 ULP. -``` -Returns `Err(DomainError)` for x = 0. +It proves the near-one shortcut separately at `< 0.5` raw units and the +`m == SCALE` branch at `< 1.499803`. It covers all 128 input bit lengths, 253 +exhaustive normalization intervals, every reachable exponent `k=-40..88`, all +1,024 midpoint segments, 132,096 segment/exponent pairs, every stored constant, +every fixed-point rounding/truncation step, discarded normalization bits, and +all relevant integer-overflow bounds. The proof is pinned to a digest of the +exact Rust kernel bodies, so a kernel edit invalidates the certificate until it +is reviewed and regenerated. + +**Retained empirical result.** The 100,000 production and 10,000 full-width +adversarial vectors both measured 2 ULP maximum, 1 ULP P99, and zero median. +The corpus result is tighter than the proved 3-ULP all-input bound. `x = 0` +returns `Err(DomainError)`. The full certificate, source digests, error +decomposition, and limitations are recorded in +`.superstack/ln-proof-certificate-2026-07-12.md` and its JSON companion. + +
+Historical 16-entry arctanh/Remez proof (superseded; not applicable to the current code) **Implementation overview.** Table-assisted Remez-polynomial arctanh with 16-entry split-constant lookup and combined sub-ULP correction. @@ -965,132 +1094,88 @@ Rounded to a clean bound: **|f-hat(x) - ln(x/SCALE) * SCALE| <= 3** for all x > *Remark (Empirical validation).* Max observed error = 3 ULP across 100K production vectors (production suite: ln_fixed_i max ULP = 3, median 1). +
+ ----- ### Proposition 8 (exp_fixed_i — fixed-point exponential) -**Source:** `transcendental.rs:93-141` +**Source:** `src/transcendental.rs` and generated `src/exp_coeffs.rs`. -**Classification:** Category B — analytical conservative bound. Remez rational -approximation (FreeBSD msun style) with split LN2 correction. +**Classification:** Category B — source-bound numerical and exact-integer +certificate. The certificate is reproducible with +`python3 scripts/certify_exp_fixed.py` and is retained as +`.superstack/exp-proof-certificate-2026-07-12.{md,json}`. -**Statement.** For all x in [-40*SCALE, 40*SCALE], +**Statement.** For every successful non-saturated input +`-40*SCALE < x < 40*SCALE`, the certified fixed-point source model has combined +relative error below ``` -|exp_fixed_i(x) - exp(x/SCALE)*SCALE| <= C * 2^k +1.54994794363012e-16. ``` -where k = floor(x / LN2_I) (after correction), C <= 4 ULP in the pre-reconstruction -sum. The relative error is bounded independently of k: - -``` -|f-hat(x) - exp(x/SCALE)*SCALE| / |exp(x/SCALE)*SCALE| < 4*sqrt(2) / SCALE ≈ 5.7 * 10^-12. -``` - -Returns `Ok(0)` for x <= -40*SCALE, `Err(Overflow)` for x >= 40*SCALE. - -**Implementation overview.** Remez rational formula with 7 rounding operations -(vs 24 in the previous Taylor series). - -1. **Domain guards** (`transcendental.rs:94-98`): Returns `Ok(0)` for x <= -40*SCALE, - `Err(Overflow)` for x >= 40*SCALE, `Ok(SCALE_I)` for x = 0. -1. **Range reduction with split LN2** (`transcendental.rs:103-117`): - - `k = x / LN2_I` — initial octave estimate - - `ln2_correction = round(k * LN2_LO / SCALE_I)` — sub-ULP residual correction. - `LN2_I` overshoots true `ln(2)*SCALE` (since `LN2_LO < 0`), so `k*LN2_I` is too - large and `r = x - k*LN2_I` is too small. The correction adds back the deficit. - - `r = x - k * LN2_I - ln2_correction` — reduced argument - - Boundary adjustment: if `|r| > LN2_I/2`, adjust k by ±1 and r by ∓LN2_I - - After reduction: `|r/SCALE| <= ln(2)/2 ≈ 0.347` -1. **Remez rational formula** (`transcendental.rs:120-133`): - - `xx = fp_mul_i_round(r, r)` — r squared (1 rounding op) - - Degree-4 Horner in xx: - `poly = P1 + xx*(P2 + xx*(P3 + xx*(P4 + xx*P5)))` (4 rounding ops via - `fp_mul_i_round`) - - `c = r - fp_mul_i_round(poly, xx)` — correction term (1 rounding op) - - `rc = fp_mul_i_round(r, c)` — (1 rounding op) - - `sum = SCALE_I + r + fp_div_i(rc, 2*SCALE_I - c)` — rational combination - (1 division) -1. **Reconstruction** (`transcendental.rs:136-140`): `sum << k` for k >= 0, - `sum >> |k|` for k < 0. Uses `checked_shl` with `Err(Overflow)` on overflow. - -**Constants used** (from `constants.rs`): - -- `LN2_I = 693_147_180_560` — round(ln(2) × SCALE), error +0.055 ULP -- `LN2_LO = -54_690_582_768` — sub-ULP residual of LN2_I -- `EXP_REMEZ_P1 = 166_666_666_667` (≈ 1/6 × SCALE) -- `EXP_REMEZ_P2 = -2_777_777_778` (≈ -1/360 × SCALE) -- `EXP_REMEZ_P3 = 66_137_563` -- `EXP_REMEZ_P4 = -1_653_390` -- `EXP_REMEZ_P5 = 41_381` - -*Proof.* The error decomposes into four components. - -*(A) Range reduction error.* The split LN2 approach computes -`r = x - k * LN2_I - round(k * LN2_LO / SCALE_I)`. The residual after correction -is bounded by the rounding of `k * LN2_LO / SCALE_I`, which is <= 0.5 ULP. -This is a factor of ~|k| better than the uncorrected approach (which would have -error `|k| * 0.055` ULP). **Contribution: <= 0.5 ULP** in r. +The corresponding conservative absolute raw bounds are 41,159 for `|x| < 20` +and `2.20965e13` over the full guarded domain. Absolute error grows with the +reconstructed power of two; relative error is the stable metric. The public +guards return `Ok(0)` for `x <= -40*SCALE` and `Err(Overflow)` for +`x >= 40*SCALE`. -Since `d(exp(r))/dr = exp(r)` and the sum ≈ SCALE, a 0.5 ULP error in r propagates -as `0.5 * exp(r_real) / SCALE` ≈ 0.5 ULP (since `exp(r_real) ∈ [1/√2, √2]`). -**Range reduction contributes <= 0.7 ULP to the sum.** - -*(B) Rational formula rounding.* The formula involves 7 rounding operations: - -1. `xx = fp_mul_i_round(r, r)` — 0.5 ULP -2. `fp_mul_i_round(xx, P5)` — 0.5 ULP (attenuated by subsequent multiplications) -3. `fp_mul_i_round(xx, poly)` — 0.5 ULP (×3 more of these) -4-5. Two more Horner steps — 0.5 ULP each -6. `fp_mul_i_round(poly, xx)` for c — 0.5 ULP -7. `fp_mul_i_round(r, c)` for rc — 0.5 ULP - -The Horner steps (2-5) compute `poly ≈ P1 ≈ 0.167 * SCALE` (the higher-order terms -are negligible for |r/SCALE| < 0.35). The polynomial error is attenuated by the -subsequent `poly * xx` multiplication: `|xx/SCALE| <= 0.347^2 = 0.120`, so Horner -errors contribute `< 2.0 * 0.120 = 0.24 ULP` to c. - -The `xx` error (0.5 ULP) propagates through `c = r - poly*xx` as -`0.5 * |poly/SCALE| = 0.5 * 0.167 = 0.083 ULP`. - -The `rc = r * c` and `rc / (2*SCALE - c)` steps: since `|c/SCALE| < 0.06` and -`|rc/SCALE| < 0.02`, the division error from `fp_div_i` is negligible relative to -the sum (which is ≈ SCALE). - -**Total rounding in sum: < 1.5 ULP.** - -*(C) Approximation error.* The Remez rational formula `1 + r + r*c/(2-c)` with -`c = r - P(xx)*xx` approximates `exp(r)` with error bounded by the Remez exchange -algorithm. For |r/SCALE| <= 0.347, the approximation error of the degree-9 rational -form is < 10^-14 relative, i.e. < 0.01 ULP at SCALE. **Negligible.** - -*(D) Reconstruction amplification.* The bit shift `sum << k` multiplies both result -and error by 2^k. The relative error is invariant under this scaling. - -**Combined bound:** - -|Error source |Contribution to sum| -|----------------------------|-------------------| -|(A) Range reduction (split) |<= 0.7 ULP | -|(B) Rational formula rounding|< 1.5 ULP | -|(C) Approximation error |< 0.01 ULP | -|**Total pre-reconstruction**|**< 2.3 ULP** | - -Conservative clean bound: C = 4. - -**Absolute error:** <= 4 × 2^k ULP. **Relative error:** < 4√2 / SCALE ≈ 5.7 × 10^-12. - -For |x| <= 20*SCALE: |k| <= 28, absolute bound <= 1.1 × 10^9. -For |x| <= 40*SCALE: |k| <= 57, absolute bound <= 5.8 × 10^17. -The relative bound (5.7 × 10^-12) holds across the full ±40×SCALE range. □ - -*Remark (Empirical validation).* Max observed = 473M ULP at i128 boundary (consistent -with 4 × 2^k amplification); max 1 ULP in financial domain (|x| < 20*SCALE). -Production suite: 100K vectors. +**Implementation overview.** -*Remark (Domain guards).* The code returns `Ok(0)` for x <= -40*SCALE (underflow -to zero) and `Err(Overflow)` for x >= 40*SCALE. These are documented boundary -behaviours; the high-side overflow is reported through `Result`. +1. The correctly-rounded direct branch returns `SCALE+x` on + `-1_000_000 <= x < 1_000_000`. +2. An i64 reciprocal proposes the nearest full-ln(2) octave. The rounded raw + residual is converted to Q63 by a split reciprocal (`18_446_744` plus a + Q28 fractional limb), avoiding both division and a wide reduction multiply. + A Q96 correction restores the sub-raw residual of decimal `LN2_I`. +3. The octave residual is split into 32 cells. An exact Q63 boundary correction + confines `r` to `[-ln(2)/64, ln(2)/64]`. +4. A degree-5 true-Remez polynomial is evaluated by five Q63 multiply-shifts + with Q22 output guards. +5. One of 32 Q62 constants reconstructs `2^(phase/32)`. Phase, octave, and all + remaining guard bits are combined at the single final rounding point. + +The numerical payload is six i64 coefficients plus 32 i64 reconstruction +constants: 304 bytes. The reconstruction constants are exact rounded +fractional powers of two, not sampled exponential answers. + +*Proof outline.* The executable certificate parses the exact generated +constants and source hashes, then composes four independently checked terms. + +*(A) Range reduction.* Exact width arithmetic bounds every i64 product. The +split reciprocal plus LN2 correction differs from the ideal Q63 residual by at +most `70.9221` Q63 units. The low-precision octave/cell reciprocals are only +proposals; exact residual comparisons correct either proposal by at most one. +All 3,694 reachable ln(2)/32 decision seams were exhaustively checked at 17 +adjacent raw inputs (`62,798` checks): zero wrong cells and zero monotonic +reversals. + +*(B) Approximation and integer Horner.* High-precision Remez exchange gives the +stored degree-5 polynomial. Arb interval evaluation plus exact modeling of Q22 +coefficient quantization and each Q63 multiply-shift bounds the local polynomial +error by `7.03916600143633e-17` on the complete reduced interval. + +*(C) Reconstruction and widths.* The certificate includes Q62 factor +quantization, the phase product, and the single final rounding. Every i64/i128 +intermediate is bounded below its type maximum; the most demanding phase +product uses less than one quarter of `i128::MAX`. Reconstruction preserves the +relative bound while absolute raw error scales with `2^octave`. + +Composing (A)–(C) gives the stated `1.54994794363012e-16` relative bound and +the 41,159 / `2.20965e13` financial/full-domain raw envelopes. □ + +*Empirical validation.* The exact 100,000-case `[-20,20]` corpus measured +max/P99/P95/median `33,622 / 7,881 / 192 / 0`. The regenerated 10,000-case +adversarial corpus brackets every reduction seam and measured +`15,727,361,334,177 / 6,704,999,717,817 / 224,176,707,138 / 0`. The former +rational kernel on the same corpora measured maxima `449,129,270` and +`395,478,324,222,178,273`, respectively. + +*Scope.* This proves the parsed fixed-point source model and generated +constants. Deployed-SBF differential measurements and downstream reference +campaigns are separate evidence; it is not a formal proof of LLVM or the SBF +virtual machine. ----- @@ -1391,8 +1476,9 @@ output = checked_mul_div_u(int_result, frac_result, SCALE) into a single correction before rounding. Split LN2 correction via `LN2_HP` + `LN2_HP_LO`. Error: <= 2 ULP at HP scale. -- `exp_fixed_hp` (`hp.rs:181-216`): Remez rational HP exponential. Same structure - as `exp_fixed_i` (Proposition 8) but at SCALE_HP with HP-specific coefficients +- `exp_fixed_hp` (`hp.rs:181-216`): Remez rational HP exponential. This HP-only + path retains the former rational structure independently of the standard-scale + phased minimax kernel, using SCALE_HP-specific coefficients (`EXP_REMEZ_HP_P1..P5`). Uses `fp_mul_hp_fast` and `fp_div_hp_safe`. Split LN2 correction via `LN2_HP` + `LN2_HP_LO`. Error: <= 3 ULP at HP scale. @@ -1534,36 +1620,37 @@ consistent with the attenuation effect dominating in practice. |----------------------------|----|------------------------------------------------------|-----------------------|-----------------------------------| |fp_mul_i (Lemma 1) |A |< 1 (truncation remainder) |1 |returns Err on overflow | |fp_sqrt Path B (Prop. 1) |A |< 1 (exact by 256-bit bisection) |1 |x*SCALE overflows u128 | -|fp_sqrt Path A (Prop. 1) |A/B |< 1 (Newton convergence arg.) |1 |x*SCALE fits in u128 | +|fp_sqrt Path A (Prop. 1) |A |< 1 (Newton invariant + checked square + exact restart)|1 |x*SCALE fits in u128 | |checked_mul_div_i (Lemma 3) |A |0 (exact) |0 |returns Err on overflow | |fp_div / fp_div_i (Lemma 2) |A |exact floor (unsigned) / trunc (signed); remainder < 1 |1 |b != 0, result fits | |fp_mul_hp_u / _i / _fast (Prop. 2)|A|<= 0.5 (rounding) |0 |no overflow | |fp_div_hp_safe (Prop. 3) |A |exact trunc; remainder < 1 |— |b != 0, returns Ok | |checked_mul_div_floor/ceil_i (Prop. 4)|A|0 (exact) |0 |returns Err on overflow | |fp_div_floor / fp_div_ceil (Prop. 5)|A|0 (exact rounding) |0-1 |b != 0, result fits | -|norm_cdf_poly (Prop. 6) |B |<= 9 (cert 2.27 + overhead 6) |4 |\|x\| <= 8*SCALE | -|ln_fixed_i (Prop. 7) |B |<= 3 (analytical, sub-ULP correction) |3 |x > 0, result fits i128 | -|exp_fixed_i (Prop. 8) |B |<= 4 * 2^k; rel < 5.7e-12 |473M (boundary), 1 (financial)|\|x\| <= 40*SCALE | +|norm_cdf_poly (Prop. 6) |B |real < 2.393619; nearest-integer <= 2 ULP; monotone |2 |all `i128`; saturates outside ±8S | +|ln_fixed_i (Prop. 7) |B |real < 2.925564; nearest-integer <= 3 ULP |2 |x > 0, result fits i128 | +|exp_fixed_i (Prop. 8) |B |rel < 1.55e-16; raw <= 41,159 financial / 2.21e13 full |33,622 production / 15.73T adversarial|\|x\| <= 40*SCALE | |sin_core (Prop. 9a) |B |< 4 (certified approx 0.10 + analyt. overhead) |2 (sin_fixed, 100K) |\|x\| <= pi/4 * SCALE | |cos_core (Prop. 9b) |B |< 4 (certified approx 0.16 + analyt. overhead) |2 (cos_fixed, 100K) |\|x\| <= pi/4 * SCALE | |norm_cdf_poly_hp (Prop. 10) |B |<= 12 (cert 3.46 + overhead 8.5); tail <= 18 |5 |\|x\| <= 8*SCALE_HP | |pow_fixed_hp (Prop. 11) |B |<= 5 (output<=100*S); rel < 4.4e-14 |1 (moderate), 21.5M (extreme) |base∈[0.01,100]*S, exp∈[0,20]*S | |norm_pdf (Prop. 12) |B |<= 14 |2 |\|x\| <= 8*SCALE | -**Category key:** A = exact integer proof. A/B = exact with analytical convergence -argument. B = analytical conservative bound (worst-case accumulation and/or Lipschitz -certificate). +**Category key:** M = bit-precise machine-checked property. A = exact integer +proof. B = interval-certified and/or analytical conservative bound. The +machine-checked integer layer composes with the Category A rows; whole functions +still require the stated arithmetic premises and induction around each harness +boundary. ‡ `fp_div_hp_safe` (Proposition 3) uses a widened fallback when the intermediate `|r| * SCALE_HP` overflows i128. The result is exact for `Ok` values; unrepresentable quotients return `Err(Overflow)`. -† The code accepts inputs up to ±40×SCALE, returns `Ok(0)` for underflow below -that range, and returns `Err(Overflow)` above that range. The error analysis in -Proposition 8 covers only ±20×SCALE. The relative -error bound (< 1.7 × 10^-11) is valid for the full ±40×SCALE range; the absolute -bound formula (12 × 2^k) is valid but gives much larger values at |x| > 20×SCALE. -See Proposition 8 domain discrepancy remark. +† The code accepts inputs up to ±40×SCALE, returns `Ok(0)` at and below the +negative guard, and returns `Err(Overflow)` at and above the positive guard. +Proposition 8 covers the complete successful interval: relative error is below +`1.55e-16`, with conservative raw envelopes 41,159 for `|x| < 20` and +`2.210e13` over the full guarded domain. **Notes on the table.** @@ -1690,7 +1777,7 @@ of failure. |`pow_fixed_hp` |base ∈ [0.01, 100]×S, exp ∈ [0, 20]×S (Prop. 11)|**UNDERFLOWS/ERR** — returns exact special cases such as x^0 and 0^y, returns `Ok(0)` when the fixed-point result underflows, and returns `Err(Overflow)` when the result is too large. Negative base impossible (u128). |Safe | |`sin_core` |\|x\| ≤ π/4 × SCALE (Prop. 9a) |**PANICS** (debug) / **SILENT** (release) — `debug_assert!` added; public `sin_fixed` performs octant reduction before calling. |Mitigated (internal)| |`cos_core` |\|x\| ≤ π/4 × SCALE (Prop. 9b) |**PANICS** (debug) / **SILENT** (release) — identical to sin_core. Public `cos_fixed` performs range reduction. |Mitigated (internal)| -|`norm_cdf_poly` |\|x\| ≤ 8×SCALE (Prop. 6) |**CLAMPS** — returns 0 for x < −8×SCALE, SCALE for x > 8×SCALE. Post-clamp on polynomial output to [0, SCALE]. |Safe | +|`norm_cdf_poly` |all `i128` (Prop. 6) |**CLAMPS BY DESIGN** — returns 0 for x < −8×SCALE, SCALE for x > 8×SCALE. Those tails and the post-clamp are included in the all-input error and monotonicity certificate. |Safe / proved | |`norm_cdf_poly_hp`|\|x\| ≤ 8×SCALE_HP (Prop. 10) |**CLAMPS** — returns 0 for x < −8×SCALE_HP, SCALE_HP for x > 8×SCALE_HP. Post-clamp on polynomial output to [0, SCALE_HP]. |Safe | |`norm_pdf` |\|x\| ≤ 8×SCALE (Prop. 12) |**UNDERFLOWS** — returns 0 in extreme tails after an explicit `-x²/2 < -40×SCALE` guard or unreachable `exp_fixed_i` overflow. |Safe | |`fp_div_hp_safe` |scaled quotient fits i128 (Prop. 3) |**ERR** — returns `Err(DivisionByZero)` for b = 0 and `Err(Overflow)` for unrepresentable quotients; uses U256 fallback for large remainder products. |Safe | @@ -1719,86 +1806,34 @@ non-decreasing: `norm_cdf_poly(x + δ) >= norm_cdf_poly(x)` for all δ > 0. A monotonicity violation means P(S < K₁) > P(S < K₂) for K₁ < K₂ — a negative implied density that creates phantom arbitrage signals. -### Analysis at single-ULP input resolution +### Standard-scale all-input result -At standard scale (SCALE = 10^12), adjacent integer inputs x and x+1 represent -real values differing by 10^-12. The true CDF increment between them is: +At `SCALE = 10^12`, the true CDF increment between adjacent raw inputs is less +than one output unit, so empirical sweeps cannot establish monotonicity. In fact, +the exact checker found two one-unit reversals in earlier versions that 110,000 +reference vectors and a dense sweep missed. -``` -Φ((x+1)/S) × S - Φ(x/S) × S ≈ φ(x/S) -``` +The final Q23-body/Q39-tail implementation has a stronger result. For every +piece, Arb interval subdivision proves the exact quantized polynomial derivative +has the required strict sign. The Q44 input map is monotone; whenever it changes, +it advances by at least 70 Q44 units. The exact adjacent polynomial increment in +evaluation-guard units is then compared with the complete two-endpoint Horner +rounding uncertainty. Every piece has positive margin: -where φ is the standard normal PDF. This increment is at most φ(0) ≈ 0.399 — less -than 0.4 output ULP even at the peak of the PDF. The output is therefore a staircase -function that stays flat across many consecutive integer inputs and occasionally -increments by 1. +- narrowest body margin: `12.395604... > 7` in the 4.5–5 piece; +- narrowest tail margin: `8.368553... > 3` in the 6.5–7 piece. -With the proved error bound of ±43 ULP (Proposition 6), adjacent inputs could in -theory produce outputs differing by up to 86 ULP in the "wrong" direction — if the -error function swung from +43 to -43 across a single input step. However, the -Lipschitz analysis constrains the rate of change of the error function. For the -worst piece (I3, [3.0, 5.0]): +Therefore each rounded piece is nondecreasing at single-raw-input resolution. +Exact checks cover the center, every cross-piece seam, the 7-sigma constant-tail +transition, the half-raw tail cutoff, and saturation. Symmetry proves the +negative half. This establishes -``` -L ≈ 2700 per real unit (derived from L × h/2 = 0.027, h = 2 × 10^-5) -Error change per integer ULP = L / SCALE ≈ 2.7 × 10^-9 +```text +norm_cdf_poly(x + 1) >= norm_cdf_poly(x) ``` -The error function changes by less than 3 × 10^-9 per integer input step — -effectively constant. **Adjacent-input monotonicity violations are theoretically -possible but constrained to at most 1 ULP in magnitude** (from the staircase -rounding, not from error swings). These are inherent to any fixed-point CDF -implementation and are not specific to this library. - -### Minimum separation for guaranteed monotonicity - -The meaningful question is: at what input separation δ is monotonicity guaranteed? -This requires the true CDF increment to exceed twice the maximum possible error -swing over that interval: - -``` -φ(x/S) × δ > 2 × L × δ / S + 2 × (staircase rounding) -``` - -Since the Lipschitz error change over interval δ is `L × δ / S`, and L/S ≈ 2.7 × 10^-9, -this is negligible compared to φ(x/S) for all |x| ≤ 5×SCALE (where φ ≥ 1.49 × 10^-6). -The binding constraint is the staircase rounding: the output must increment by at least -2 to guarantee that rounding cannot reverse the ordering. - -**Output increments by region:** - -|Input region (σ)|\|x\|/SCALE|φ(x/S)|Output increment per input ULP|Steps for +2 output increment| -|----------------|-----------|------|------------------------------|-----------------------------| -|Near 0 |0 |0.399 |0.399 |~5 | -|1σ |1 |0.242 |0.242 |~9 | -|2σ |2 |0.054 |0.054 |~37 | -|3σ |3 |0.0044|0.0044 |~454 | -|4σ |4 |0.0001|0.0001 |~17,000 | -|5σ |5 |1.5e-6|1.5e-6 |~1,340,000 | - -At 3σ, monotonicity is guaranteed over input separations of ~454 ULP (= 4.54 × 10^-10 -in real terms). At 5σ, the minimum separation grows to ~1.34 × 10^6 ULP -(= 1.34 × 10^-6 in real terms). - -### Practical implications - -For option pricing, the relevant granularity is the strike price increment, not -single-ULP fixed-point steps. A typical strike increment of $0.01 on a $100 underlying -corresponds to a standardised price ratio change of ~10^-4, which is ~10^8 input ULP -at SCALE. This vastly exceeds the monotonicity threshold at any σ level in the table -above. - -**Conclusion:** Single-ULP monotonicity violations are inherent to fixed-point CDF -computation and cannot be eliminated at this precision without increasing SCALE. -However, monotonicity at any practically relevant pricing granularity is guaranteed -by the Lipschitz smoothness of the error function combined with the dominance of the -true CDF increment over the error variation at separations above ~10^3 ULP. - -**Recommendation:** If strict monotonicity at single-ULP resolution is required for -a specific application (e.g., an AMM invariant), implement a simple post-hoc -monotonicity wrapper: `max(result, prev_result)` when evaluating the CDF at -increasing x values. This adds no error (it only clips spurious decreases of ≤ 1 ULP) -and guarantees monotone output for any sorted input sequence. +for every `x < i128::MAX`, including the far clamps. No caller-side wrapper or +minimum input separation is required for the standard-scale implementation. ### HP scale @@ -1812,10 +1847,12 @@ relevant input separation, the true CDF increment dominates the error variation. ## Appendix: Compute unit budget (Solana) -CU measurements from Solana localnet production benchmark runs. Most medians come -from 50,000 on-chain vectors per function (`BENCH_CONCURRENCY=32`); avg/max values -come from the current benchmark tables where available. All values are Solana -compute units. +Current changed-path figures come from the final deployed SBF consumer: +2,000 stratified inputs per numerical path (`BENCH_CONCURRENCY=32`) plus +explicit branch grids. All values bracket the SolMath call only. The complete +affected-path matrix is in +`.superstack/exp-composite-cu-revalidation-2026-07-12.md`; unaffected branch-grid +and certificate rows remain in `.superstack/accuracy-cu-revalidation-2026-07-11.md`. ### Individual function CU cost @@ -1824,60 +1861,82 @@ compute units. |fp_div |625 |655 |690 |Current NUC arithmetic rerun | |fp_div_i |652 |676 |724 |Current NUC arithmetic rerun | |checked_mul_div_i|883 |883 |3,807 |U256 path when product overflows u128 | -|fp_sqrt |3,598 |3,007 |9,402 |Thin path vs overflow path | +|fp_sqrt |3,796 |3,801 |9,027 |Thin path vs overflow path | |fp_mul_hp_i |103 |103 |103 |Current optimized HP multiply | -|ln_fixed_i |4,562 |4,362 |5,207 |Table-assisted Remez | -|ln_fixed_hp |19,175 |18,889 |19,764 |Compensated HP path | -|sin_fixed |4,654 |4,029 |5,170 |Includes range reduction | -|cos_fixed |4,578 |4,027 |5,181 |Includes range reduction | -|norm_cdf_poly |6,844 |6,186 |15,333 |Varies by piece and tail path | +|ln_fixed_i |705 |739 |808 |1,024-segment Q42 cubic midpoint kernel | +|ln_fixed_hp |20,090 |20,101 |21,040 |Compensated HP path | +|sin_fixed |5,072 |5,127 |6,204 |Includes range reduction | +|cos_fixed |5,044 |5,115 |5,895 |Includes range reduction | +|norm_cdf_poly |960 |983 |993 |Q23 body/Q39 tail; no exp/division | +|exp_fixed_i |961 |962 |992 |N32/Q63 degree-5 phased minimax | +|norm_pdf |2,262 |2,323 |2,337 |One optimized exp plus fixed multiply | +|pow_fixed |2,535 |2,543 |2,727 |ln -> mul -> optimized exp | |norm_cdf_poly_hp |24,234 |19,708 |40,691 |High variance: short-circuit vs polynomial | -|pow_fixed_hp |27,408 |27,408 |35,000 |ln -> mul -> exp chain | +|pow_fixed_hp |28,914 |29,137 |30,041 |ln -> mul -> exp chain | ### Composite workflow CU cost |Workflow / function |Avg CU |Median CU|Max CU | |--------------------------|-------|---------|-------| -|**bs_full (standard)** |50,191 |50,015 |68,418 | -|**bs_full_hp (HP)** |118,202|116,628 |164,961| -|barrier_option |262,906|261,773 |385,456| -|pow_product_hp |16,000 |— |20,000 | -|nig_64 |344,273|346,648 |386,010| -|implied_vol |156,563|148,575 |395,940| -|bvn_cdf |128,614|129,700 |153,090| -|Phi2Table.eval |943 |943 |943 | +|**bs_full (standard)** |24,717 |25,021 |25,650 | +|**bs_full_hp (HP)** |113,177|112,816 |149,925| +|barrier_option |270,156|270,835 |415,531| +|pow_product_hp |37,114 |37,151 |38,541 | +|NIG certified pricing |129,872|28,261 |381,385| +|implied_vol |88,707 |82,917 |328,660| +|Heston deterministic |118,523|117,092 |183,239| +|SABR guarded price |283,712|282,052 |603,172| +|bvn_cdf |100,498|110,822 |135,944| +|bvn_cdf_hp |248,294|276,569 |307,310| +|Phi2Table.eval |1,439 |1,439 |1,440 | ### Budget assessment -**Standard Black-Scholes (bs_full):** 68K CU worst case — fits comfortably within +**Standard Black-Scholes (bs_full):** 26K CU worst case — fits comfortably within the default 200K CU transaction budget. Leaves ample room for surrounding program logic (account deserialization, state updates, CPI calls). -**HP Black-Scholes (bs_full_hp):** 165K CU worst case in the benchmark set — fits -within 200K CU but with limited headroom (~35K CU remaining). For transactions that require additional +**HP Black-Scholes (bs_full_hp):** 150K CU worst case in the benchmark set — fits +within 200K CU but with limited headroom (~50K CU remaining). For transactions that require additional computation beyond pricing (e.g., settlement logic, multi-leg evaluation), a CU budget increase to 400K should be requested. This is standard practice on Solana and has minimal gas cost impact. -**NIG model (nig_64):** 386K CU worst case — requires a CU budget increase and -may need architectural consideration (e.g., splitting across instructions) if -combined with other computation. +**NIG model:** the bounded exponential-NIG engine uses a Chernoff tail tier and +an embedded 15/7 direct-OTM density integral. The final 2K deployed campaign +measured 367,321 P99 / 381,385 max math CU and 382,441 max full-instruction CU. +The committed 100K/10K accuracy campaign found no returned-allowance or +requested-error violations. The allowance is empirically cross-validated, not +a formal all-domain Gauss-Kronrod remainder proof. + +**Implied volatility (implied_vol):** 329K CU observed maximum — request at +least 500K plus integration headroom. Iterative solvers are variable. + +**Barrier options:** 416K CU observed maximum — request at least 500K plus +integration headroom. -**Implied volatility (implied_vol):** 396K CU worst case — requires a CU budget -increase. Iterative solvers are inherently variable; fast convergence is much -cheaper while edge cases can approach the upper bound. +**Deterministic Heston:** 184K CU observed maximum — request at least 250K. +Stochastic positive-expiry Heston fails closed before quadrature. -**Barrier options:** 385K CU worst case — require a CU budget increase. +**Guarded SABR:** 604K CU observed accepted-case maximum — request at least +700K, and do not execute without a complete surface certificate. ----- -## Appendix: Empirical validation summary +## Appendix: Final empirical validation summary Production benchmark suite results (100K stratified vectors per function) superseding the per-result empirical remarks above. Where adversarial vectors were run (10K targeting known weak spots), those results are also included. -### Proved bounds vs observed maxima +The figures below were rerun on the final audit tree. Post-exp changed-path +percentiles and downstream reference results are in +`.superstack/exp-downstream-revalidation-2026-07-12.md`; retained unaffected +results, model rejection counts, independent-reference methodology, and the +broader SBF matrix are in +`.superstack/accuracy-cu-revalidation-2026-07-11.md`. + +### Derived bounds vs observed maxima |Function |Proved bound (this document) |Observed max (production)|Observed max (adversarial)|Ratio (prod)|Status | |-----------------|---------------------------------|-------------------------|--------------------------|------------|-------------------| @@ -1888,63 +1947,65 @@ vectors were run (10K targeting known weak spots), those results are also includ |fp_sqrt |≤ 1 (Prop. 1) |1 |— |— |✓ Consistent | |fp_mul_hp_i |≤ 0.5 (Prop. 2) |0 |— |— |✓ Consistent | |fp_div_hp_safe |≤ 1 (Prop. 3) |1 |— |— |✓ Consistent | -|ln_fixed_i |≤ 15 (Prop. 7) |7 |6 |2.1× |✓ Conservative | -|ln_fixed_hp |≤ 15 (Prop. 7 analysis) |8 |— |1.9× |✓ Conservative | -|exp_fixed_i |≤ 12 × 2^k (Prop. 8) |1.5 × 10^9 (production) |6.0 × 10^17 (adversarial) |— |✓ See note 1 | -|pow_fixed_hp |≤ 5 for output ≤ 14×S (Prop. 11) |112 × 10^6 |118.5 × 10^6 |— |✓ See note 2 | -|norm_cdf_poly |≤ 43 (Prop. 6) |42 |9 (deep tails) |1.02× |✓ Tight | +|ln_fixed_i |<= 3 integer ULP (Prop. 7) |2 |2 |1.5x |Conservative | +|ln_fixed_hp |≤ 15 (Prop. 7 analysis) |2 |2 |7.5× |✓ Conservative | +|exp_fixed_i |rel < 1.55e-16 (Prop. 8) |33,622 |15,727,361,334,177 |— |✓ See note 1 | +|pow_fixed_hp |≤ 5 for output ≤ 14×S (Prop. 11) |21.5 × 10^6 |41.9 × 10^6 |— |✓ See note 2 | +|norm_cdf_poly |<= 2 integer ULP (Prop. 6) |2 |2 |1x |Tight | |norm_cdf_poly_hp |≤ 8 / ≤ 17 (Prop. 10) |5 |— |1.6× / 3.4× |✓ Conservative | -|sin_fixed (full) |< 4 (Prop. 9a, core bound) |2 |— |2× |✓ Conservative | -|cos_fixed (full) |< 4 (Prop. 9b, core bound) |2 |— |2× |✓ Conservative | -|norm_pdf |≤ 14 (Prop. 12) |2 |— |7× |✓ Very conservative| - -**Note 1 (exp_fixed_i):** The large absolute errors are expected and consistent with -the 2^k amplification structure (Proposition 8). At the adversarial range [25, 39.5]×SCALE -(within the code's ±40×SCALE guard but beyond the ±20×SCALE analysis domain), the observed -maximum of 6.0 × 10^17 is below the theoretical bound of 12 × 2^57 ≈ 1.7 × 10^18. -The significant-figures metric is more informative: median 12.0 SF (production), -confirming the relative error bound of ~1.7 × 10^-11 holds across the domain. - -**Note 2 (pow_fixed_hp):** The observed maximum of 112M vastly exceeds the proved +|sin_fixed (full) |< 4 (Prop. 9a, core bound) |2 |2 |2× |✓ Conservative | +|cos_fixed (full) |< 4 (Prop. 9b, core bound) |2 |2 |2× |✓ Conservative | +|norm_pdf |≤ 14 (Prop. 12) |2 |2 |7× |✓ Very conservative| + +**Note 1 (exp_fixed_i):** Absolute raw error still grows with power-of-two +reconstruction, so the adversarial maximum occurs in the positive tail. The +seam-complete maximum `1.573e13` is below the certified `2.210e13` full-domain +envelope. Median error is zero on both retained corpora, and the relative +source-model bound is below `1.55e-16`. + +**Note 2 (pow_fixed_hp):** The observed maximum of 21.5M exceeds the derived absolute bound of ≤ 5 for moderate outputs (Proposition 11). This is not a contradiction: the ≤ 5 bound applies only when the output is ≤ 14×SCALE. Large outputs amplify absolute error via the 2^k reconstruction in exp. The significant- figures metric (median 14.3, worst 10.9) confirms the relative error bound of < 3.2 × 10^-13 holds. For the adversarial suite (near-1 cancellation + overflow), -worst-case 10.9 SF is still excellent. The standard-scale `pow_fixed` (62.7G max, +worst-case relative precision remains the meaningful metric. The standard-scale `pow_fixed` (35.5G max, 10.0 worst SF) is significantly less precise — **HP should be the default path for any precision-sensitive computation.** ### Black-Scholes end-to-end accuracy -**HP path (bs_full_hp):** Max error 4 ULP on call/put price at HP scale (10^15). +**HP path (bs_full_hp):** Max error 3/4 raw units on call/put output at SCALE (10^12). Cross-validated against QuantLib at 14.2 median significant figures for call/put prices — this exceeds the precision of IEEE 754 double-precision arithmetic (15.9 decimal digits, but typically ~14-15 SF after a chain of transcendental evaluations). |Greek |Max abs error (HP)|Median SF vs QuantLib| |----------|------------------|---------------------| -|Call price|4 |14.2 | +|Call price|3 |14.2 | |Put price |4 |14.2 | |Delta |1 |12.2 | |Gamma |1 |10.1 | -|Vega |9 |14.3 | -|Theta |3 |13.8 | -|Rho |22–23 |14.0–14.5 | +|Vega |5 |14.3 | +|Theta |1 |13.8 | +|Rho |11 / 10 |14.0–14.5 | -The Rho error (22–23 HP ULP) is the largest among the Greeks. This is expected: +The Rho error (11/10 raw units) is the largest among the HP Greeks. This is expected: Rho involves a multiplication by time-to-expiry T, which amplifies errors from the CDF and exp chain. At 14.0 SF it remains well within practical requirements. -**Standard path (bs_full):** Max error 37K ULP at standard scale (10^12). This is -orders of magnitude worse than HP, driven primarily by the standard-scale exp/pow -chain. Median SF of 11.1–11.2 for call/put is adequate for many applications but -leaves less headroom. The adversarial suite shows 6K max for calls, 5K for puts. +**Standard path (bs_full):** Max call/put errors were 2,509/2,543 raw units at +standard scale (10^12). This is orders of magnitude worse than HP because the +standard-scale composition retains less precision across its CDF, discounting, +and arithmetic steps. Median SF of 11.1–11.2 for call/put is adequate for many +applications but leaves less headroom. The adversarial suite maxed at 891/1,042 +raw units for calls/puts. -**Recommendation:** Use `bs_full_hp` for all pricing paths where accuracy matters. -The CU cost premium (118K vs 50K average) is justified by the 3+ orders of magnitude -improvement in accuracy. Reserve `bs_full` for gas-constrained paths where -approximate pricing is acceptable (e.g., indicative quotes, UI display values). +**Recommendation:** Use `bs_full_hp` where its extra precision is required and +the CU budget permits it. It averages 113,177 CU versus 24,717 CU for `bs_full`, +about a 4.6x premium. Use `bs_full` when its measured ~11 significant figures are +adequate and CU is the tighter constraint (for example, indicative quotes or UI +display values). ### Distribution of errors @@ -1953,14 +2014,14 @@ bounds are rarely approached: |Function |P50|P95|P99|Max| |----------------|---|---|---|---| -|ln_fixed_i |1 |3 |4 |7 | -|norm_cdf_poly |3 |12 |18 |42 | +|ln_fixed_i |0 |1 |1 |2 | +|norm_cdf_poly |0 |1 |2 |2 | |norm_cdf_poly_hp|0 |2 |2 |5 | -|bs_full_hp.call |0 |1 |1 |4 | +|bs_full_hp.call |0 |1 |1 |3 | |bs_full_hp.delta|0 |0 |0 |1 | |bs_full_hp.gamma|0 |0 |0 |1 | -For the HP Black-Scholes path, 73.2% of call prices are computed exactly (0 error) +For the HP Black-Scholes path, 74.5% of call prices are computed exactly (0 error) and 99% are within 1 ULP. This is consistent with the proved bounds being conservative worst-case accumulations that rarely coincide in practice. @@ -2003,7 +2064,8 @@ Cross-validated against Python arbitrary-precision arithmetic across 76,070 vect ## Appendix: Benchmark formal bounds sync -The benchmark suite's `CLAIMED_BOUND_STD` for `norm_cdf_poly` has been updated from -53 to 43 (matching the Lipschitz-certified value in Proposition 6). The `ln_fixed_i` -bound in this document is ≤ 15 (conservative ceiling of 14.5); the benchmark uses 14. -Both are valid ceilings — the difference is a rounding convention. +The former 43-ULP CDF and 15-ULP logarithm bounds applied to superseded +implementations. They must not be attached to the current Q44 CDF or Q42 +midpoint logarithm. The current source-bound certificates prove at most 2 +integer ULP for `norm_cdf_poly` and at most 3 integer ULP for `ln_fixed_i`; +their retained production and adversarial corpora both observe maxima of 2. diff --git a/README.md b/README.md index 0c77f51..4065555 100644 --- a/README.md +++ b/README.md @@ -1,593 +1,308 @@ # SolMath -Financial math that fits on Solana. +Deterministic fixed-point mathematics and quantitative finance for Rust and +Solana. [![Crates.io](https://img.shields.io/crates/v/solmath.svg)](https://crates.io/crates/solmath) [![Docs.rs](https://docs.rs/solmath/badge.svg)](https://docs.rs/solmath) [![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](LICENSE-MIT) -- **9-22x faster** than `rust_decimal` for transcendentals, **10-23x faster** than `brine-fp` -- **Standard Black-Scholes price + all 5 Greeks in ~50K CU** — HP path averages ~118K CU -- **10-14 sig figs** vs QuantLib on the HP Black-Scholes path -- **Proved error bounds** for core primitives ([PROOFS.md](PROOFS.md)) -- **European barrier options** — all 4 types (down/up × in/out), ~263K CU, validated against QuantLib on 443K vectors -- **Reproducible validation** — compact fixtures ship in the crate; full 2.5M+ vector generation/checks live in the repo +SolMath is a `no_std`, zero-dependency library for deterministic decimal math, +probability functions, derivatives pricing, and DeFi calculations. Heavily CU optimised to fit on Solana and all functions thoroughly verified against at least 100k +quantlib vectors for accuracy. Runtime +code uses integer arithmetic with 12 decimal places; there is no floating point, +heap allocation, network access, or off-chain pricing service. -`no_std` | zero dependencies | pure integer arithmetic +The crate spans the complete path from arithmetic primitives to on-chain +pricing engines: -## The Problem +- checked fixed-point multiply, divide, square root, rounding, and U256-backed + intermediate arithmetic; +- `ln`, `ln(1+x)`, `exp`, `exp(x)-1`, powers, trigonometry, and high-precision + variants; +- normal PDF/CDF/inverse CDF, bivariate normal probabilities, and two-asset + rainbow options; +- Black–Scholes prices and Greeks, implied volatility, barrier options, + arithmetic-Asian/TWAP settlement, American options, exponential NIG, + deterministic Heston, and SABR; +- weighted-pool swaps, token conversion, and explicit settlement rounding. -`rust_decimal` with its `maths` feature costs **97,188 CU (median) for one ln()** — a 4-token weighted pool needs 4 ln calls minimum, burning ~400K CU on logarithms alone. Solana programs have a hard 200,000 CU limit per instruction. +`no_std` · zero dependencies · pure integer runtime · `SCALE = 1e12` -SolMath computes ln() in **3,500-5,200 CU**. - -Measured on-chain (50,000 production vectors, Solana localnet): - -| Operation | rust_decimal | brine-fp | SolMath | vs rust_decimal | vs brine-fp | -|-----------|-------------|----------|---------|-----------------|-------------| -| ln(x) | 97,188 med CU | 41,815 med CU | 4,362 med CU | **22x** | **10x** | -| exp(x) | 29,172 med CU | 18,972 med CU† | 5,145 med CU | **6x** | **4x** | -| sqrt(x) | 19,883 med CU | 77,322 med CU | 3,007 med CU | **7x** | **26x** | -| Full BS + all Greeks | — | — | ~50,000 CU | — | — | - -†brine-fp exp only handles non-negative inputs. - -## Usage - -```rust -use solmath::*; - -// fp("...") parses decimal strings into SCALE = 1e12 fixed-point integers. -// Use it in tests, clients, scripts, and off-chain config. On-chain programs -// should receive already-validated integers across instruction data. - -let s = fp("100")?; // spot = $100 -let k = fp("105")?; // strike = $105 -let r = fp("0.05")?; // risk-free rate = 5% -let sigma = fp("0.20")?; // volatility = 20% -let t = fp("1")?; // time to expiry = 1 year - -let greeks = bs_full_hp(s, k, r, sigma, t)?; -// greeks.call ≈ $8.02 -// greeks.gamma ≈ 0.0198 -// greeks.vega ≈ 39.67 -``` +## Quick start ```toml [dependencies] -solmath = "0.1" -``` - -### Feature Flags - -Default features are `transcendental + complex`. For on-chain programs that only need specific functionality, disable defaults and pick what you need: - -```toml -# AMM pool math only — smallest binary -solmath = { version = "0.1", default-features = false, features = ["pool"] } - -# Black-Scholes pricing + IV -solmath = { version = "0.1", default-features = false, features = ["iv"] } - -# Heston stochastic vol -solmath = { version = "0.1", default-features = false, features = ["heston"] } +solmath = "0.2" ``` -| Feature | Modules | Dependencies | -|---------|---------|--------------| -| *(core)* | arithmetic, mul_div, overflow, encoding, constants, error, double_word | — | -| `transcendental` | ln, exp, pow, sin, cos, norm_cdf, norm_pdf, HP variants | — | -| `complex` | complex arithmetic | transcendental | -| `bs` | Black-Scholes pricing + Greeks | transcendental | -| `iv` | implied volatility solver | bs | -| `barrier` | European barrier options | transcendental | -| `nig` | NIG fat-tail pricing | transcendental, complex | -| `heston` | Heston stochastic vol | bs, complex | -| `sabr` | SABR stochastic vol | transcendental | -| `pool` | weighted pool swap math | transcendental | -| `bivariate` | bivariate normal CDF (GL6 + table lookup) | transcendental | -| `table-gen` | offline Φ₂ table generation | bivariate | -| `full` | production runtime modules | transcendental, complex, bs, iv, barrier, nig, heston, sabr, pool, bivariate | -| `pade-iv` | experimental Padé IV guess | iv | - -Default features: **core + transcendental + complex** — everything needed for general-purpose fixed-point math, logarithms, exponentials, trigonometry, and normal distribution. Pricing models (BS, IV, Heston, SABR, barrier, NIG), pool math, and bivariate CDFs are opt-in. Use `features = ["full"]` for production runtime modules, or `default-features = false` for core arithmetic only. `table-gen`, `pade-iv`, and `idl-build` remain explicit opt-ins. - -### Binary Size - -Deployed `.so` sizes measured against an Anchor baseline (151 KB). Rent rate: 6,960 lamports/byte (2-year rent-exempt). - -| Feature | Adds | Rent | -|---------|------|------| -| Core arithmetic (mul, div, sqrt) | +15 KB | 0.10 SOL | -| Pool math (weighted swap) | +50 KB | 0.35 SOL | -| SABR vol surface | +68 KB | 0.47 SOL | -| Black-Scholes + Greeks (HP) | +77 KB | 0.54 SOL | -| Transcendentals (ln, exp, pow, CDF) | +83 KB | 0.58 SOL | -| Heston stochastic vol | +120 KB | 0.83 SOL | -| Implied volatility solver | +161 KB | 1.12 SOL | -| Full library | +261 KB | 1.82 SOL | - -All well under Solana's 10 MB program limit. LTO strips unused code paths even within enabled features. Rent is a one-time refundable deposit. - -## Use Cases - -- **Options protocols** — Black-Scholes pricing + Greeks + IV in a single instruction -- **Exotic options** — European barrier options (knock-in/out) on-chain -- **AMMs / weighted pools** — Balancer-style swap math with overflow-safe division -- **Structured products** — fat-tail pricing (NIG) for skew-aware valuation -- **Risk engines** — HP path gives 10+ sig figs for settlement and margin calculations -- **Any on-chain math** — ln, exp, pow, sqrt, sin, cos, CDF all fit in tight CU budgets - -## Safety Model - -- **No panics in the production public API** — invalid domains, overflow, division by zero, and non-convergence return `Result` errors -- **Explicit failure modes** — invalid domains, overflow, division by zero, and non-convergence return `SolMathError`; documented clamps/underflows are intentional boundary behavior -- **Error variants:** `DomainError` (invalid input), `Overflow` (result too large), `DivisionByZero`, `NoConvergence` (iterative methods) -- **No silent decimal truncation** — `fp("...")` rejects non-zero digits beyond 12 decimal places -- **Overflow detection:** `fp_mul`, `fp_mul_i`, `fp_mul_round`, `fp_mul_i_round` return `Err(Overflow)` on overflow — no silent saturation or wrap-around. Use `checked_mul_div_i` for an exact multiply-then-divide in one step -- **Internal arithmetic:** Remez polynomials for ln/exp, boundary-constrained CDF — all validated on 100K+ vectors - -## Validation & Audit Status - -See [VALIDATION.md](VALIDATION.md) for exact release commands, package checks, -reference assets, and the production-readiness matrix. No independent -third-party audit is claimed; treat SolMath as unaudited financial -infrastructure until your integration has its own review. - -Copy-paste Solana integration paths live in [INTEGRATION.md](INTEGRATION.md). -Runnable examples live in [examples/](examples/), including options pricing, -weighted pool swaps, safe token conversion, and an Anchor instruction template. - -## At a Glance - -| Function | Median err | Max $ error | Avg CU | Max CU | -|----------|-----------|-------------|--------|--------| -| **bs_full_hp** | **0** | **$0.000000000004** | **118K** | **165K** | -| black_scholes_price_hp | 0 | $0.000000000004 | ~60K | ~80K | -| bs_full | 209 | $0.000003 | 50K | 68K | -| barrier_option | 1 | $0.000002 | 263K | 385K ¹ | -| implied_vol | 4 ⁴ | — | 157K | 396K ¹ | -| pow_fixed_hp | 0 | — | 27K | 35K | -| pow_product_hp | 1 | — | 16K | 20K | -| nig_64 | 2,520 | $0.06 | 344K | 386K ¹ | -| **bvn_cdf** | **2** ⁵ | **—** | **129K** | **153K** | -| Phi2Table.eval | 2 ⁵ | — | 943 | 943 | -| ln_fixed_i | 1 | — | 4.5K | 5.2K | -| ln_fixed_hp | 0 | — | 19K | 20K | -| exp_fixed_i | 1 ² | — | 5K | 5K | -| norm_cdf_poly | 0 | — | 6K | 15K | -| fp_sqrt | 0 | — | 3K | 9K | - -¹ Requires `ComputeBudgetProgram.setComputeUnitLimit()`. Request 500K for `barrier_option`, `implied_vol`, and `nig_64`. All other functions fit within the default 200K CU budget. - -² exp max error of 473M occurs at the i128 overflow boundary (|x| ≈ 40). Within the financial domain (|x| < 20), max error is 1 ULP. - -⁵ bvn_cdf and Phi2Table.eval: median error 2 ULP. Max error 92K ULP at |ρ| > 0.95 (= 9.2×10⁻⁵ absolute probability). For |ρ| ≤ 0.90, max error < 1 ULP. Validated on 590K vectors (mpmath 50-digit reference) + 20K on-chain CU measurements. - -Accuracy from 100K stratified offline vectors (mpmath 50-digit reference). CU from 50K on-chain vectors (NUC localnet, `BENCH_CONCURRENCY=32`). - -
-HP Black-Scholes — 100K vectors, outputs >= $0.01 - -| Greek | % Exact | Worst SF | Median SF | Max abs err | -|-------|---------|----------|-----------|-------------| -| Call | 74.5% | 9.6 | 13.6 | 3 | -| Put | 73.1% | 9.9 | 13.6 | 4 | -| Call Delta | 99.9% | 10.1 | 11.8 | 1 | -| Put Delta | 99.9% | 10.3 | 11.7 | 1 | -| Gamma | **100%** | 10.5 | 10.5 | 1 | -| Vega | 84.2% | 10.0 | 13.8 | 6 | -| Call Theta | 95.1% | 10.0 | 13.5 | 2 | -| Put Theta | 94.9% | 10.1 | 13.3 | 2 | -| Call Rho | 73.7% | 9.9 | 14.0 | 11 | -| Put Rho | 75.3% | 10.0 | 14.2 | 11 | - -
- -
-vs QuantLib 1.41 — 5,000 HP Black-Scholes vectors - -Cross-checked against [QuantLib](https://www.quantlib.org/) 1.41's BlackCalculator (IEEE 754 f64). - -| Greek | Median agreement (sig figs) | -|-------|---------------------------| -| Call | 14.2 | -| Put | 14.1 | -| Delta | 12.2 | -| Gamma | 10.1 | -| Vega | 14.3 | -| Theta | 13.6 | -| Rho | 14.5 | - -
- -
-Barrier Options — 443K vectors vs QuantLib 1.41 - -Validated against QuantLib's AnalyticBarrierEngine (Rubinstein-Reiner closed form). All 4 barrier types × call/put = 8 configurations. - -| Type | Vectors | Max ULP | P99 | Median | -|------|---------|---------|-----|--------| -| down_out_call | 60,480 | 26 | 14 | 1 | -| down_in_call | 60,480 | 23 | 10 | 0 | -| down_out_put | 50,400 | 48 | 22 | 1 | -| down_in_put | 50,400 | 63 | 27 | 1 | -| up_out_call | 50,400 | 1,654 | 27 | 1 | -| up_in_call | 50,400 | 1,654 | 33 | 1 | -| up_out_put | 60,480 | 551 | 13 | 1 | -| up_in_put | 60,480 | 552 | 12 | 0 | -| **conservation** | **443,520** | **26** | **15** | **1** | - -Conservation: in + out = vanilla, verified to ≤ 26 ULP across all 443K vectors. - -On-chain CU (10K vectors on Solana localnet): avg **263K**, median 262K, P99 321K, max 385K. - -
- -## Performance - -Measured on Solana BPF with runtime inputs (no constant folding). Median CU from 50,000 on-chain vectors per function (NUC localnet, `BENCH_CONCURRENCY=32`); avg/P99/max from earlier 100K run where not superseded. - -| Function | Avg CU | Median CU | P95 CU | P99 CU | Max CU | -|----------|--------|-----------|--------|--------|--------| -| fp_sqrt | 3,598 | 3,007 | — | 5,930 | 9,402 | -| sin_fixed | 4,654 | 4,029 | — | 5,159 | 5,170 | -| cos_fixed | 4,578 | 4,027 | — | 5,168 | 5,181 | -| exp_fixed_i | 4,935 | 5,145 | — | 5,205 | 5,212 | -| norm_cdf_poly | 6,844 | 6,186 | — | 15,311 | 15,333 | -| ln_fixed_i | 4,562 | 4,362 | 5,143 | 5,189 | 5,207 | -| pow_fixed_hp | 27,408 | 27,408 | — | — | — | -| ln_fixed_hp | 19,175 | 18,889 | 19,471 | 19,537 | 19,764 | -| norm_cdf_poly_hp | 24,234 | 19,708 | — | 40,668 | 40,691 | -| **bs_full** | **50,191** | **50,015** | — | **65,762** | **68,418** | -| **bs_full_hp** | **118,202** | **116,628** | — | **163,359** | **164,961** | -| barrier_option | 262,906 | 261,773 | 320,907 | 320,907 | 385,456 | -| implied_vol | 156,563 | 148,575 | — | 339,535 | 395,940 | -| nig_64 | 344,273 | 346,648 | — | 382,667 | 386,010 | -| **bvn_cdf** | **128,614** | **129,700** | **147,771** | — | **153,090** | -| Phi2Table.eval | 943 | 943 | 943 | 943 | 943 | - -A full Black-Scholes price + all 5 Greeks fits in **50K CU average**. The HP variant with every Greek at 10+ sig figs fits in **118K CU average**. Both leave room for protocol logic within the default 200K budget. European barrier options (all 4 types) average **263K CU** with a 400K compute budget. - -### NUC Arithmetic Rerun - -Measured on NUC localnet (`BENCH_CONCURRENCY=32`), 50,000 vectors per function. - -| Function | Avg CU | Median CU | P99 CU | Max CU | Max ULP | -|----------|--------|-----------|--------|--------|---------| -| fp_mul | 557 | 530 | 744 | 744 | 1 | -| fp_mul_i | 587 | 561 | 774 | 775 | 0 | -| fp_div | 625 | 655 | 684 | 690 | 1 | -| fp_div_i | 652 | 676 | 718 | 724 | 0 | -| fp_mul_hp_i | 103 | 103 | 103 | 103 | 0 | -| fp_div_hp | 1,376 | 1,345 | 1,480 | 1,486 | 1 | -| checked_mul_div_i | 883 | 883 | 1,106 | 3,807 | 0 | - -## Bivariate Normal CDF - -First fixed-point bivariate normal CDF on any blockchain. Two tiers: general (any ρ) and fast (fixed ρ with precomputed table). Feature-gated behind `bivariate`. - -```toml -solmath = { version = "0.1", features = ["bivariate"] } -``` - -### `bvn_cdf` — general, any ρ +Values are integers scaled by `1_000_000_000_000`. The parsing helpers are +useful in tests and clients; Solana instructions normally receive encoded +integers directly. ```rust -use solmath::{bvn_cdf, SCALE}; +use solmath::{fp, fp_div, fp_mul, fp_sqrt}; -// Φ₂(-0.5, 0.3; 0.85) — all i128 at SCALE, like everything else in SolMath -let a = -500_000_000_000i128; -let b = 300_000_000_000i128; -let rho = 850_000_000_000i128; -let prob = bvn_cdf(a, b, rho)?; // ≈ 0.271 × SCALE -``` - -6-point Gauss-Legendre quadrature (Drezner-Wesolowsky). Validated against mpmath 50-digit reference on 590K production + adversarial vectors. 20K on-chain CU measurements. +let amount = fp("1250.50")?; +let rate = fp("0.035")?; -| Metric | Value | -|--------|-------| -| CU median | 129K | -| CU max | 153K | -| Accuracy (|ρ| ≤ 0.90) | max error < 4×10⁻⁷ | -| Accuracy (|ρ| ≤ 0.95) | max error < 5×10⁻⁶ | -| Accuracy (|ρ| ≤ 0.99) | max error < 10⁻⁴ | -| Properties | monotone, symmetric (exact), non-negative, bounded | +let interest = fp_mul(amount, rate)?; // 43.7675 +let one_third = fp_div(fp("1")?, fp("3")?)?; // 0.333333333333 +let root_two = fp_sqrt(fp("2")?)?; // 1.414213562373 +``` -### `Phi2Table` — fast, fixed ρ +Pricing models use the same representation: ```rust -use solmath::Phi2Table; +use solmath::{bs_full_hp, SCALE}; -// Embed a precomputed table as const (generated offline with `table-gen` feature) -const MY_TABLE: Phi2Table = Phi2Table::from_array(/* 64×64 i32 array */); +let greeks = bs_full_hp( + 100 * SCALE, // spot + 105 * SCALE, // strike + 50_000_000_000, // rate = 5% + 200_000_000_000, // volatility = 20% + SCALE, // one year +)?; -let prob = MY_TABLE.eval(-500_000_000_000i128, 300_000_000_000i128)?; +// greeks.call, greeks.put, greeks.call_delta, greeks.gamma, greeks.vega, ... ``` -Catmull-Rom bicubic interpolation on a 64×64 lookup table. Pure i64 arithmetic — no `norm_cdf`, no i128 in the hot path. - -| Metric | Value | -|--------|-------| -| CU | 943 (constant) | -| Accuracy | max error < 9.0×10⁻⁵ | -| Storage | 64 KB const per table | -| Properties | non-negative, bounded, monotone to 10⁻⁴ | - -Enable `table-gen` for the offline `Phi2Table::generate(rho, 64)` constructor. Not needed for on-chain evaluation. - -## Accuracy - -Validated against 3M+ offline test vectors (100K stratified production per function + 443K barrier vectors from QuantLib + 10K adversarial + 1.35M original suite) plus 1M on-chain vectors on Solana localnet. References computed with mpmath at 50-digit precision, cross-checked against scipy and QuantLib. - -### Full accuracy table (100K production vectors) - -| Function | Max err | P99 | P95 | Median | % Exact | Max $ err ² | -|----------|---------|-----|-----|--------|---------|------------| -| fp_mul_i | 0 | 0 | 0 | 0 | 100% | — | -| fp_div_i | 0 | 0 | 0 | 0 | 100% | — | -| checked_mul_div_i | 0 | 0 | 0 | 0 | 100% | — | -| fp_sqrt | 1 | 1 | 1 | 0 | 50.0% | — | -| fp_mul_hp_i | 0 | 0 | 0 | 0 | 100% | — | -| fp_div_hp_safe | 1 | 1 | 1 | 1 | 49.6% | — | -| ln_fixed_i | 3 | 2 | 2 | 1 | 44.2% | — | -| ln_fixed_hp | 2 | 1 | 1 | 0 | 71.7% | — | -| exp_fixed_i | 473M ³ | 127M | 3.5M | 1 | 32.0% | — | -| sin_fixed | 2 | 1 | 1 | 1 | 48.9% | — | -| cos_fixed | 2 | 1 | 1 | 1 | 44.6% | — | -| norm_cdf_poly | 4 | 3 | 2 | 0 | 50.2% | — | -| norm_cdf_poly_hp | 5 | 3 | 2 | 1 | 42.8% | — | -| norm_pdf | 2 | 1 | 1 | 1 | 23.8% | — | -| pow_fixed_hp | 21.5M | 648 | 0 | 0 | 96.1% | — | -| pow_product_hp | 3K | 1K | 518 | 1 | 45.3% | — | -| bs_full.call | 3K | 2K | 1K | 209 | 1.6% | $0.000003 | -| bs_full.put | 3K | 2K | 1K | 213 | 2.2% | $0.000003 | -| bs_full_hp.call | 3 | 1 | 1 | 0 | 74.5% | $0.000000000003 | -| bs_full_hp.put | 4 | 2 | 1 | 0 | 73.1% | $0.000000000004 | -| bs_full_hp.delta | 1 | 0 | 0 | 0 | 99.9% | — | -| bs_full_hp.gamma | 1 | 0 | 0 | 0 | 100% | — | -| bs_full_hp.vega | 6 | 1 | 1 | 0 | 84.2% | — | -| bs_full_hp.call_theta | 2 | 1 | 0 | 0 | 95.1% | — | -| bs_full_hp.put_theta | 2 | 1 | 1 | 0 | 94.9% | — | -| bs_full_hp.call_rho | 11 | 2 | 1 | 0 | 73.7% | — | -| bs_full_hp.put_rho | 11 | 2 | 1 | 0 | 75.3% | — | -| barrier (down call) | 26 | 14 | 8 | 1 | — | $0.000000000026 | -| barrier (down put) | 63 | 27 | 13 | 1 | — | $0.000000000063 | -| barrier (up call) | 1,654 | 33 | 17 | 1 | — | $0.000000001654 | -| barrier (up put) | 552 | 13 | 7 | 1 | — | $0.000000000552 | -| nig_64 | 64K | 49K | 16K | 2,520 | — | $0.06 | -| implied_vol | 17M ⁴ | 20.5K | 47 | 4 | — | — | -| bvn_cdf | 92K ⁵ | 9.6K | 864 | 2 | — | — | -| Phi2Table.eval | 90K ⁵ | — | — | 2 | — | — | - -² Dollar errors assume a ~$10 option. 1 ULP = $0.000000000001. - -³ exp max error 473M occurs at the i128 overflow boundary (|x| ≈ 40). Within the financial domain (|x| < 20), exp achieves 10+ significant figures. The relative error remains < 1.7 × 10⁻¹¹ across the full range. - -⁴ IV ULP measured via round-trip: σ_true → BS price (mpmath) → quantize to SCALE → `implied_vol` → compare to σ_true. Offline Rust measurement on 100K production + 10K adversarial vectors; 108,494 converging inputs (98.6%). 1,506 inputs return `Err(NoConvergence)` — deep ITM/OTM where extrinsic value is below 1 ULP and there is no invertible signal. 96.2% of converging inputs are within the 100 ULP design tolerance; the tail (max 17M ULP, 0.24% of inputs) occurs near the convergence boundary where price quantization limits recoverable precision. CU from 50K on-chain vectors (NUC localnet). - -Accuracy from 100K stratified offline vectors (mpmath 50-digit reference). - -
-Formal error bounds - -| Function | Proved bound | Observed max | -|----------|-------------|-------------| -| fp_mul_i | < 1 (proved) | 0 | -| fp_mul_round | ≤ 0.5 (by construction) | 0 | -| fp_mul_i_round | ≤ 0.5 (by construction) | 0 | -| fp_div_round | ≤ 0.5 (by construction) | — | -| fp_sqrt | < 1 (proved) | 1 | -| checked_mul_div_i | 0 exact (proved) | 0 | -| ln_fixed_i | <= 15 (proved) | 3 | -| ln_fixed_hp | <= 15 (proved) | 2 | -| norm_cdf_poly | <= 5 (certified) | 4 | - -See [PROOFS.md](PROOFS.md) for complete proofs. - -
- -
-

Functions

- -### Pricing and Greeks +## What is included -```rust -// Price + all 5 Greeks in one call — ~50K CU -bs_full(s, k, r, sigma, t) -> Result - -// HP price only (no Greeks) — ~60K CU -black_scholes_price_hp(s, k, r, sigma, t) -> Result<(u128, u128), SolMathError> - -// High-precision variant — ~118K CU, 10+ sig figs on every Greek -bs_full_hp(s, k, r, sigma, t) -> Result - -// Implied volatility — Li (2006) rational guess → Halley → Jäckel fallback, ~157K CU avg / 148K median -// Returns Err(NoConvergence) for sub-ULP extrinsic (deep ITM) or zero-vega cases -implied_vol(market_price, s, k, r, t) -> Result - -// NIG fat-tail pricing (i64/1e6 scale, ~344K CU on-chain) -nig_call_64(s, k, r, t, alpha, beta, delta) -> Result -nig_put_64(s, k, r, t, alpha, beta, delta) -> Result - -// NIG i128 variant — offline/high-precision only (~302K CU native, exceeds on-chain budget) -nig_call_price(s, k, r, t, alpha, beta: i128, delta) -> Result - -// European barrier options — ~263K CU, 4 types × call/put -barrier_option(s, k, h, r, sigma, t, is_call, barrier_type) -> Result -// BarrierResult { price: u128, vanilla: u128 } -// BarrierType: DownAndOut, DownAndIn, UpAndOut, UpAndIn - -// Individual Greeks (all return Result, all need sigma > 0 and t > 0) -black_scholes_price(s, k, r, sigma, t) -> Result<(u128, u128), SolMathError> -bs_delta(s, k, r, sigma, t) -> Result<(i128, i128), SolMathError> // (call_delta, put_delta) -bs_gamma(s, k, r, sigma, t) -> Result -bs_vega(s, k, r, sigma, t) -> Result -bs_theta(s, k, r, sigma, t) -> Result<(i128, i128), SolMathError> // (call_theta, put_theta) -bs_rho(s, k, r, sigma, t) -> Result<(i128, i128), SolMathError> // (call_rho, put_rho) -``` +| Area | Main APIs | +|---|---| +| Fixed-point arithmetic | `fp_mul`, `fp_div`, `fp_sqrt`, rounded/floor/ceil variants, `checked_mul_div_*`, `DoubleWord` | +| Transcendentals | `ln_fixed_i`, `ln_1p_fixed`, `exp_fixed_i`, `expm1_fixed`, powers, sine, cosine | +| Probability | normal PDF/CDF/inverse CDF, bivariate normal CDF, certified fixed-correlation tables | +| European options | Black–Scholes prices, all Greeks, implied volatility, barriers | +| Path-dependent options | partially fixed arithmetic-Asian and TWAP settlement | +| American options | Kim boundary reconstruction plus early-exercise-premium integration | +| Alternative distributions | European exponential-NIG pricing with call, put, error allowance, and execution tier | +| Volatility models | deterministic Heston reduction, SABR implied volatility/pricing/surface certification | +| Multi-asset options | best-of and worst-of calls via the Stulz bivariate-normal formula | +| DeFi math | weighted-pool swaps, token conversion, explicit payout/collection rounding | -### Transcendentals +## Feature selection -```rust -ln_fixed_i(x: u128) -> Result // 4.5K CU, 3 ULP max (table-assisted) -exp_fixed_i(x: i128) -> Result // 5K CU, 1 ULP median (see accuracy table) -pow_fixed(base, exp) -> Result // via exp(exp * ln(base)) -pow_fixed_hp(base, exp) -> Result // 1 ULP median, ~27K CU, tested up to 100×SCALE -pow_int(base: u128, n: u128) -> Result // integer power, split recursion -pow_fixed_i(base: i128, exp: i128) -> Result // signed power -ln_fixed_hp(x: i128) -> Result // HP variant, 2 ULP max, ~19K CU (compensated DW) -exp_fixed_hp(x: i128) -> Result // HP variant at 1e15 scale -sin_fixed(x: i128) -> Result // 2 ULP max, ~5K CU -cos_fixed(x: i128) -> Result // 2 ULP max, ~5K CU -sincos_fixed(x: i128) -> Result<(i128, i128), SolMathError> // both at once, shared reduction -``` +The default is core arithmetic plus `transcendental`. Financial models and +complex arithmetic are opt-in so downstream programs compile only the +capabilities they use. -### Normal Distribution +```toml +# Core arithmetic only +solmath = { version = "0.2", default-features = false } -```rust -norm_cdf_poly(x: i128) -> Result // Phi(x), piecewise minimax, ~7K CU, 4 ULP -norm_pdf(x: i128) -> Result // phi(x) = exp(-x^2/2)/sqrt(2pi), 2 ULP -norm_cdf_and_pdf(x: i128) -> Result<(i128, i128), SolMathError> // both at once -norm_cdf_poly_hp(x: i128) -> Result // HP variant, 5 ULP at 1e15 scale, ~24K CU -bvn_cdf(a: i128, b: i128, rho: i128) -> Result // Phi2(a,b;rho), ~129K CU -bvn_cdf_hp(a: i128, b: i128, rho: i128) -> Result // offline/table-generation reference -Phi2Table::from_array(values: [[i32; 64]; 64]) -> Phi2Table -Phi2Table::eval(&self, a: i128, b: i128) -> Result // fixed-rho lookup, 943 CU -Phi2Table::generate(rho: i128, n: usize) -> Result // requires table-gen -``` +# Black–Scholes and Greeks +solmath = { version = "0.2", default-features = false, features = ["bs"] } -### Arithmetic +# Fully on-chain American pricing +solmath = { version = "0.2", default-features = false, features = ["american-kbi"] } -```rust -fp(decimal: &str) -> Result // decimal parser for off-chain/test config -fp_i(decimal: &str) -> Result // signed decimal parser -fp_mul(a: u128, b: u128) -> Result // truncating -fp_mul_round(a: u128, b: u128) -> Result // rounding (≤ 0.5 ULP) -fp_mul_i(a: i128, b: i128) -> Result // truncating -fp_mul_i_round(a: i128, b: i128) -> Result // rounding (≤ 0.5 ULP) -fp_mul_i_round_dw(a: i128, b: i128) -> Result // rounding + sub-ULP remainder -fp_div(a: u128, b: u128) -> Result // truncating, overflow-safe via U256 -fp_div_round(a: u128, b: u128) -> Result // rounding (≤ 0.5 ULP) -fp_div_i(a: i128, b: i128) -> Result // signed, overflow-safe -fp_div_floor(a, b) -> Result -fp_div_ceil(a, b) -> Result -checked_mul_div_i(a, b, c) -> Result // (a * b) / c, exact via U256 -checked_mul_div_floor_i(a, b, c) -> Result // floor rounding -checked_mul_div_ceil_i(a, b, c) -> Result // ceil rounding -mul_div_floor(a: u64, b: u64, c: u64) -> Result // u64 mul-div, floor -mul_div_ceil(a: u64, b: u64, c: u64) -> Result // u64 mul-div, ceil -mul_div_floor_u128(a: u128, b: u128, c: u128) -> Result // u128 mul-div via U256 -mul_div_ceil_u128(a: u128, b: u128, c: u128) -> Result // u128 mul-div via U256 -fp_sqrt(x: u128) -> Result // Newton-Raphson, 1 ULP -fp_mul_hp_i(a: i128, b: i128) -> Result // HP multiply at 1e15 scale -fp_mul_hp_u(a: u128, b: u128) -> Result // HP multiply unsigned -fp_div_hp_safe(a: i128, b: i128) -> Result // HP division +# Fully on-chain exponential NIG pricing +solmath = { version = "0.2", default-features = false, features = ["nig"] } + +# All stable runtime modules +solmath = { version = "0.2", features = ["full"] } ``` -### Compensated Arithmetic +| Feature | Capability | Pulls in | +|---|---|---| +| `transcendental` | logs, exponentials, powers, trig, normal distribution, HP kernels | — | +| `complex` | fixed-point complex arithmetic | `transcendental` | +| `bs` | Black–Scholes prices and Greeks | `transcendental` | +| `iv` | implied-volatility solver | `bs` | +| `barrier` | continuous European barriers | `transcendental` | +| `asian` | arithmetic-Asian and partially fixed TWAP settlement | `transcendental` | +| `american-kbi` | American call/put pricing | `transcendental` | +| `nig` | exponential-NIG call/put pricing | `transcendental` | +| `heston` | deterministic-variance Heston reduction | `bs` | +| `sabr` | SABR analytics, prices, Greeks, and surface certificates | `transcendental` | +| `pool` | weighted-pool and token math | `transcendental` | +| `bivariate` | bivariate normal CDF and table evaluators | `transcendental` | +| `rainbow` | two-asset best-of/worst-of options | `bivariate` | +| `full` | every stable runtime capability above | all runtime features | +| `table-gen` | offline bivariate table generation | `bivariate` | +| `pade-iv` | alternate experimental IV initializer | `iv` | + +`table-gen` and `pade-iv` are intentionally outside `full`. For size-sensitive +Solana programs, prefer `default-features = false` with one or two explicit +capabilities. Final linked size also depends on which functions the downstream +program actually calls and what LTO removes. + +## On-chain pricing engines + +### American options: Kim Boundary Integration + +`american_kbi_price` accepts only `(S, K, r, q, sigma, T)` and the option side. +It reconstructs the smooth-pasting exercise boundary and evaluates Kim's +early-exercise-premium integral inside the program. The embedded artifact is +parameter-independent quadrature geometry and normal-kernel coefficients—not +a grid of prices or per-contract lookup data. ```rust -// DoubleWord: hi + lo/SCALE — tracks sub-ULP remainders through multiply chains -DoubleWord { hi: i128, lo: i128 } -DoubleWord::from_hi(v: i128) -> DoubleWord // wrap standard value (lo = 0) -DoubleWord::to_i128(self) -> i128 // collapse with rounding -DoubleWord::add(self, other) -> DoubleWord // exact addition with carry - -// Split LN2 constants for sub-ULP range reduction in ln/exp -LN2_LO: i128 // true_ln2 × SCALE ≈ LN2_I + LN2_LO / SCALE -LN2_HP_LO: i128 // same at HP scale -LN_REMEZ_COEFFS: [i128; 8] // ln polynomial as array -LN_REMEZ_HP_COEFFS: [i128; 10] // HP ln polynomial as array +use solmath::{american_kbi_price, AmericanKbiKind, SCALE}; + +let put = american_kbi_price( + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + 30_000_000_000, + 300_000_000_000, + SCALE, + AmericanKbiKind::Put, +)?; ``` -### Pool Math +The production corpus measured maximum call/put errors of +`$0.002744 / $0.003264` per `$100` strike against QuantLib QdFp. The deployed +quote instructions maxed at `390,628 / 390,786` CU. See +[Kim Boundary Integration](docs/AMERICAN_KBI.md). -```rust -weighted_pool_swap( - balance_in, balance_out, - weight_in, weight_out, - amount_in, fee_rate, -) -> Result<(u128, u128), SolMathError> // (net_output, fee) - -pow_product_hp(x, w) -> Result // x^w * x^(1-w) pool invariant, 13+ sig figs -token_to_fp(raw_amount: u64, decimals: u8) -> Result -fp_to_token_floor(fp_amount: u128, decimals: u8) -> Result -fp_to_token_ceil(fp_amount: u128, decimals: u8) -> Result -``` +### Exponential NIG -### Complex Arithmetic +`nig_price_certified` prices the smaller out-of-the-money leg through an +embedded 15/7 Gauss–Kronrod density integral, obtains the other leg by exact +fixed-point put-call parity, and returns the quote's numerical allowance and +execution tier. ```rust -complex_mul(a: Complex, b: Complex) -> Result -complex_div(a: Complex, b: Complex) -> Result -complex_exp(z: Complex) -> Result -complex_sqrt(z: Complex) -> Result +use solmath::{nig_price_certified, NigParams, SCALE}; + +let quote = nig_price_certified( + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + 20_000_000_000, + SCALE, + NigParams { + alpha: 15 * SCALE, + beta: -2 * SCALE as i128, + delta_per_year: SCALE, + }, + 5_000_000_000, // requested absolute error = 0.005 +)?; + +// quote.call, quote.put, quote.max_abs_error, quote.tier ``` -
- -
-

How It Works

- -**Fixed-point, not floating-point.** Everything is integer arithmetic on `u128`/`i128` with an implicit 1e12 denominator. No floats touch the runtime. - -**Range reduction.** Transcendentals are computed on small intervals and scaled back: -- **ln:** 16-entry split-constant lookup table + degree-3 Remez polynomial via arctanh substitution. Table narrows polynomial range from [0, 1/3] to [0, 1/33], cutting Horner steps from 7→3. Sub-ULP residuals on table values and LN2 constant. 3 ULP max at ~4.5K CU. HP variant uses compensated DW Horner (degree-9) for 2 ULP max. -- **exp:** Decompose x = k*ln(2) + r, Remez rational approximation on the remainder, scale by 2^k. ~half the CU of Taylor. -- **sin/cos:** Cody-Waite two-word 2pi reduction, then minimax Taylor polynomials. 2 ULP max. -- **sqrt:** Newton-Raphson with bit-length initial guess. 1 ULP. - -**Minimax polynomial CDF.** 6 piecewise degree-11 polynomials + CF8 asymptotic tail, boundary-constrained, coordinate-descent optimized. 4 ULP max, fully monotone. - -**High-precision path.** HP functions compute at 1e15 internal scale, then round to 1e12 on output. The extra 3 digits of internal precision drown truncation noise — all HP Greeks hold 10+ significant figures at ~2.7x the CU cost. - -**Overflow-safe division.** `fp_div` and `fp_div_i` use U256 widened arithmetic when `a * SCALE` would overflow u128. Fast path: ~660 CU. Widened path: ~1,650 CU. Both exact to the truncation remainder. - -**Compensated arithmetic.** `DoubleWord` tracks sub-ULP remainders: `fp_mul_i_round_dw` returns both the rounded quotient and the exact residual. `horner_compensated` (internal) propagates these remainders through polynomial evaluation, reducing accumulated error from O(n × 0.5 ULP) to O(0.5 ULP). Split LN2 constants (`LN2_LO`, `LN2_HP_LO`) enable sub-ULP range reduction corrections in ln/exp. - -**Shared intermediates.** `bs_full` computes d1, d2, Phi(d1), sigma*sqrt(T) once and reuses across price + all 5 Greeks. - -**Implied volatility.** Three-stage solver: (1) Li (2006) bivariate rational polynomial for the initial guess when |x| < 0.5 and the normalised price has meaningful digits, (2) bracketed Halley refinement (up to 4 iterations), (3) Jäckel "Let's Be Rational" normalised-space fallback with Householder(3) for out-of-Li-domain cases. Deep OTM tails use a two-step asymptotic guess: A = −2·ln(β) − ln(2π), A₂ = A − ln(A), σ√T ≈ |x|/√A₂. Deep ITM cases where the OTM-equivalent extrinsic value rounds to zero at SCALE return `NoConvergence` rather than a garbage answer. - -
- - -
-

vs Other Libraries

- -Measured on-chain, 50,000 production vectors, Solana localnet (median CU): - -| Function | SolMath | rust_decimal | brine-fp | SolMath vs rust_decimal | SolMath vs brine-fp | -|----------|---------|-------------|----------|------------------------|---------------------| -| ln | 4,362 | 97,188 | 41,815 | **22x faster** | **10x faster** | -| exp | 5,145 | 29,172 | 18,972† | **6x faster** | **4x faster** | -| sqrt | 3,007 | 19,883 | 77,322 | **7x faster** | **26x faster** | - -†brine-fp exp skips negative inputs. - -**Accuracy** (Max ULP, 50K vectors): SolMath ≤2 ULP on all three. brine-fp ≤1 ULP on all three. - -**Feature gap**: brine-fp has no Black-Scholes, Greeks, IV solver, normal CDF/PDF/inverse CDF, barrier options, NIG distribution, or pool math. rust_decimal has no transcendentals within the CU budget. - -vs **fermat-math**: fermat-math handles decimal accounting with 7 IEEE rounding modes; SolMath handles computational finance — transcendentals, distribution functions, pricing models. They're complementary. - -
- -## Testing - -Every accuracy number is independently reproducible. References computed with [mpmath](https://mpmath.org/) at 50 decimal digits, cross-checked against scipy and [QuantLib](https://www.quantlib.org/) 1.41. - -100K stratified production vectors per function (regime-bucketed, not uniform random). 443K barrier vectors from QuantLib's AnalyticBarrierEngine. 10K adversarial vectors targeting cancellation regions and overflow boundaries. Formal proofs for core primitives in [PROOFS.md](PROOFS.md). - -```bash -# From a full repository checkout: -pip install -r scripts/requirements.txt -python3 scripts/generate_production_vectors.py -python3 scripts/generate_adversarial_vectors.py -python3 scripts/generate_barrier_vectors.py -python3 scripts/crosscheck_quantlib.py -``` +Across the production/adversarial reference campaigns, maximum normalized +errors were `$0.000565 / $0.001397` per `$100`. The deployed instruction maxed +at `382,441` CU. See [Exponential NIG](docs/NIG.md). + +### Arithmetic-Asian and TWAP settlement + +`twap_option_price` combines the authenticated fixed portion of an in-progress +average with exact continuous-GBM first and second moments for the remaining +window, then prices the moment-matched distribution. It handles unseasoned, +partially fixed, and fully fixed averages through one API. The practical +deployed sweep maxed at `182,458` math CU. See +[Arithmetic-Asian / TWAP](docs/ASIAN_TWAP.md). + +### The rest of the model surface + +- `bs_full` provides a compact standard-precision price-and-Greeks path; + `bs_full_hp` provides the highest-accuracy European path. +- `implied_vol` combines a rational initializer, bracketed Halley refinement, + and a normalized-space fallback. +- `barrier_option_with_state` prices continuous down/up, in/out barriers while + incorporating the contract's persisted breach state. +- `best_of_call` and `worst_of_call` implement the two-asset Stulz formulas. +- `certify_sabr_surface` validates a complete strike/maturity grid and returns + typed certified quote nodes for execution. +- `heston_price` implements the exact deterministic-variance (`xi = 0`) + reduction to high-precision Black–Scholes. + +## Performance and footprint + +Measurements below are from deployed SBF artifacts; CU figures refer to the +math call or benchmark instruction stated, not an application's entire +transaction. + +| Operation | Typical CU | Observed maximum | +|---|---:|---:| +| `ln_fixed_i` | 705 average | 808 | +| `exp_fixed_i` | 961 average | 992 | +| `norm_cdf_poly` | 960 average | 993 | +| `bs_full` price + Greeks | 24,717 average | 25,650 | +| `bs_full_hp` price + Greeks | 113,177 average | 149,925 | +| arithmetic-Asian / TWAP | 137,997 average | 182,458 math / 186,610 adversarial instruction | +| deterministic Heston | 118,523 average | 190,756 retained branch-grid max | +| exponential NIG | 129,872 average | 382,441 full instruction | +| American KBI call / put | 381,096 / 371,876 average | 390,628 / 390,786 full instruction | + +Isolated linked SBF footprints against the same `184,848`-byte Anchor baseline: + +| Capability | Total SBF | Linked delta | +|---|---:|---:| +| `exp_fixed_i` | 191,824 bytes | 6,976 bytes | +| `expm1_fixed` + `ln_1p_fixed` | 225,800 bytes | 40,952 bytes | +| American KBI | 293,360 bytes | 108,512 bytes | +| Exponential NIG | 311,464 bytes | 126,616 bytes | + +Run `scripts/measure_sbf_footprint.sh` and the composite harness against the +exact downstream program before setting deployment and transaction budgets. + +## Accuracy and numerical design + +SolMath uses range reduction, low-degree minimax/piecewise kernels, compensated +fixed-point arithmetic, and widened intermediates. Parameter-independent +coefficients and quadrature geometry are generated offline and embedded as +integers; option prices remain functions of live inputs. + +Representative release evidence: + +| Path | Release result | +|---|---| +| Core integer arithmetic | 545/545 bit-precise Kani checks across 13 harnesses: U256 carry/division bounds, exact sqrt Newton/bisection transitions, truncation `< 1 ULP`, and nearest rounding `<= 0.5 ULP` | +| `ln_fixed_i` | all-input Arb/exact-integer certificate: at most 3 ULP | +| `norm_cdf_poly` | all-`i128` certificate: at most 2 ULP, exact symmetry, monotone | +| HP Black–Scholes | max call/put error 3/4 raw units in the 100K corpus | +| Barrier options | 443,520 QuantLib comparisons across all eight type/side combinations | +| Arithmetic-Asian / TWAP | 100K production + 10K adversarial mpmath references; production max `$2.258e-8` | +| American KBI | 100K production + 10K adversarial plus held-out/unseen QuantLib QdFp surfaces | +| Exponential NIG | 100K production + 10K adversarial references plus independent 50-digit density/Lewis checks | +| SABR | 500-case QuantLib corpus plus whole-grid parity, bound, vertical, butterfly, and calendar checks | + +The detailed corpora, commands, certificate identities, and model-specific +domains are indexed in [VALIDATION.md](VALIDATION.md) and +[PROOFS.md](https://github.com/DJBarker87/solmath/blob/v0.2.0/PROOFS.md). + +## Runtime contract + +- Public computations are infallible or return `Result<_, SolMathError>`. +- `DomainError`, `Overflow`, `DivisionByZero`, and `NoConvergence` make the + numerical outcome explicit. +- The crate forbids unsafe Rust, has no runtime dependencies, and does not + allocate. +- U256-backed paths preserve results whose final value fits even when a + `u128 × u128` intermediate does not. +- Validated input types such as `EuropeanInputs`, `TwapInputs`, and + `PoolSwapInputs` let programs establish model bounds once at the instruction + boundary. +- Settlement helpers expose floor and ceiling explicitly so protocols can + choose their economic rounding policy. + +See [SECURITY.md](SECURITY.md) for the precise arithmetic and model contracts, +and [INTEGRATION.md](INTEGRATION.md) for Anchor patterns and compute-budget +guidance. + +## Documentation + +- [Usage guide](USAGE.md) — worked examples for the runtime APIs. +- [Solana integration](INTEGRATION.md) — validated inputs, Anchor error mapping, + account examples, rounding, and CU budgets. +- [Architecture](docs/ARCHITECTURE.md) — precision tiers, modules, generated + kernels, feature gates, and rounding conventions. +- [Kim Boundary Integration](docs/AMERICAN_KBI.md) — method, domain, QuantLib + accuracy, CU, and artifact identity. +- [Exponential NIG](docs/NIG.md) — model convention, API, domain, accuracy, and + compute tiers. +- [Arithmetic-Asian / TWAP](docs/ASIAN_TWAP.md) — settlement state, moment + equations, validation, and usage. +- [Validation](VALIDATION.md) — release matrix and reproducibility commands. +- [Security model](SECURITY.md) — arithmetic guarantees and integration + boundaries. +- [API reference](https://docs.rs/solmath) — generated Rust documentation. ## License diff --git a/SECURITY.md b/SECURITY.md index 2b9cc72..c3a212d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,150 +1,158 @@ -# SolMath — Security Properties +# SolMath security model + +SolMath is designed for deterministic, value-bearing arithmetic in constrained +Rust runtimes. Its security model is based on explicit numeric domains, checked +intermediates, bounded execution, typed validation at program boundaries, and +reproducible numerical evidence. -This document covers the safety guarantees of `solmath`: panic behaviour, -overflow handling, domain checking, and the distinction between public API -contracts and internal fast paths. - ---- - -## No panics in the production public API - -All public functions are either infallible or return `Result<_, SolMathError>`. -The four error variants and their triggers: - -| Variant | When it fires | -|---------|---------------| -| `DomainError` | Input outside the mathematical domain (ln of zero, zero barrier level, token_decimals > 38, …) | -| `Overflow` | Intermediate or final result would exceed the representable integer range | -| `DivisionByZero` | Divisor is zero in a divide operation | -| `NoConvergence` | Iterative solver (implied_vol) ran out of iterations | - -The only `panic!`/`unwrap`/`expect` calls found in the crate are in tests, -rustdoc examples, test-only helpers, and `debug_assert!` statements. The -`debug_assert!` checks are compiled out in release builds, including Solana -`.so` deployments. - ---- - -## No unsafe code - -The library is entirely safe Rust. There is no `unsafe` block anywhere in -`solmath`. This is enforced by `#![forbid(unsafe_code)]` in `lib.rs`. - ---- - -## No heap allocation - -`solmath` is `#![no_std]` with zero dependencies. It never allocates. -Every value lives on the stack or in a register. This is a hard requirement -for Solana on-chain programs. - ---- - -## Overflow strategy - -The library has three layers of overflow defence: - -### 1. Checked primitive arithmetic - -Throughout the codebase, `checked_mul`, `checked_add`, `checked_sub` etc. are -the default. Any failure propagates as `Err(SolMathError::Overflow)`. - -```rust -// Example from fp_mul: -match a.checked_mul(b) { - Some(p) => Ok(p / SCALE), - None => checked_mul_div_u(a, b, SCALE).ok_or(SolMathError::Overflow), -} -``` - -### 2. U256 fallback for wide multiplications - -When a `u128 × u128` product exceeds `u128::MAX`, the library falls through to -a software U256 path rather than returning an error immediately. This means -functions like `fp_mul`, `fp_mul_i`, and the `checked_mul_div_*` family succeed -for all inputs whose *final* result fits in u128/i128, even if the intermediate -product doesn't. - -The U256 implementation (`overflow.rs`) uses 128-bit limbs and a long-division -fallback verified by `debug_assert` against the long-division reference. - -### 3. Explicit bounds guards at function entry - -High-level functions add domain-specific guards before entering arithmetic. For -example, `heston_price` rejects any parameter that exceeds `i128::MAX` or the -i64-scale limit used by the characteristic function: - -```rust -if s > i128::MAX as u128 || k > i128::MAX as u128 || … { - return Err(SolMathError::Overflow); -} -``` - ---- - -## Internal fast paths - -Two internal functions perform unchecked multiplication: - -| Function | Where used | Why it is safe | -|----------|-----------|----------------| -| `fp_mul_i_fast(a, b)` | Trig polynomial cores (`sin_core`, `cos_core`), Heston CF | Inputs are reduction-bounded; comments prove `\|a*b\| < i128::MAX` at every call site | -| `fp_mul_hp_fast(a, b)` | HP ln/exp polynomial evaluation | Callers guarantee `\|a\|, \|b\| <= ~40*SCALE_HP`; product <= 1.6e33, fits i128 (max ~1.7e38) | -| `mul_fast(a, b)` in `iv.rs` | Li rational-guess polynomial | IV solver validates inputs; comments bound each product to ~24 decimal digits < i128::MAX | - -These are `pub(crate)` and cannot be called from external code. Every call site -carries a comment explaining why no overflow can occur. - ---- - -## Angle reduction safety - -`sin_fixed` and `cos_fixed` reduce arbitrary angles via `rem_euclid`, which -handles `i128::MIN` correctly (unlike the raw `%` operator, which has -implementation-defined sign for negative dividends in many languages). The -reduced value is always in `(-π, π]` before polynomial evaluation. - ---- - -## Division-by-zero handling - -- `fp_div`, `fp_div_i`, `fp_div_floor`, `fp_div_ceil`, `fp_div_round` — return - `Err(DivisionByZero)` when the divisor is zero. -- `fp_div_hp_safe` — same. -- `weighted_pool_swap` — returns `Err(DivisionByZero)` when `weight_out == 0`. -- All `checked_mul_div_*` variants — check `c == 0` and return `None`/`Err`. - ---- - -## Convergence and iteration bounds - -`implied_vol` uses three successive methods (Li rational initial guess → Halley -→ Jaeckel rational). Each iteration loop has an explicit `max_iter` cap. If the -solver does not converge inside the cap it returns `Err(NoConvergence)` rather -than looping forever. - ---- - -## What is not guaranteed - -- **Accuracy after extreme inputs.** The library is designed for financially - realistic inputs (spot/strike in reasonable ranges, σ ∈ (0, 5), T ∈ (0, 10)). - Functions will *not panic* outside these ranges and will *attempt* to return a - result, but accuracy is not validated beyond the tested parameter space. - See `PROOFS.md` and the benchmark validation reports for tested ranges. - -- **Cryptographic security.** SolMath is financial math, not a cryptographic - library. No timing-side-channel guarantees are made. - -- **Formal verification.** Error bounds in `PROOFS.md` are analytically derived - with AI assistance but have not been independently machine-checked. - ---- - -## Audit history - -No independent third-party audit is claimed for the published crate. The -current public validation consists of reproducible reference vectors, -deterministic property-style tests, and internal review documented in -`PROOFS.md` and the generated test data. Treat financial-model use as unaudited -until your integration has its own review. +## Reporting + +Report a vulnerability privately through the repository's GitHub +**Security → Report a vulnerability** flow. The maintainer will acknowledge a +report within five business days and coordinate disclosure. The latest +published release and the current main branch receive fixes. + +## Enforced runtime properties + +| Property | Enforcement | +|---|---| +| Safe Rust | `#![forbid(unsafe_code)]` at the crate root | +| Deterministic runtime | integer-only implementation; no floating point | +| `no_std` | `#![no_std]` with zero dependencies | +| No allocation | fixed-size values and arrays; no allocator | +| Explicit failure | public computations are infallible or return `Result` | +| Bounded execution | iterative methods have fixed iteration caps | +| Overflow handling | checked primitives plus U256 widened paths | +| Profile consistency | debug and release tests run with overflow checks both on and off | +| Production panic gate | CI rejects `unwrap`, `expect`, `panic`, and `unreachable` in the library | + +The public error contract has four variants: + +| Error | Meaning | +|---|---| +| `DomainError` | The input does not define a supported mathematical problem | +| `Overflow` | The final value cannot be represented safely | +| `DivisionByZero` | A required divisor is zero | +| `NoConvergence` | An iterative/error-gated computation cannot return the requested result | + +This keeps numerical outcomes in the normal control flow of a Solana +instruction. Integrations can map each variant to their own program error and +retry, reject, or select another quote path as appropriate. + +## Arithmetic and overflow + +SolMath uses three layers of arithmetic protection. + +### Checked operations + +`checked_add`, `checked_sub`, `checked_mul`, and checked conversions are the +default throughout the implementation. A non-representable result becomes +`SolMathError::Overflow`. + +### Widened intermediates + +Multiplication followed by division frequently has a representable result even +when `a * b` does not fit in `u128`. `fp_mul`, `fp_div`, and the +`checked_mul_div_*` / `mul_div_*_u128` families use software U256 arithmetic for +that case. The widened path preserves the exact quotient/remainder relationship +before the requested rounding rule is applied. + +### Entry-domain validation + +Pricing and DeFi functions validate their numeric and relational domains before +entering sensitive kernels. The `checked` module turns those constraints into +types such as `EuropeanInputs`, `ImpliedVolInputs`, `BarrierInputs`, +`TwapInputs`, and `PoolSwapInputs`. Programs can validate raw instruction data +once and carry a valid domain through the rest of the calculation. + +## Internal optimized kernels + +A small number of `pub(crate)` multiply helpers omit repeated checks inside +already reduced polynomial domains. They are not callable by downstream code. +Their callers establish bounds before entry: + +| Internal path | Established bound | +|---|---| +| Trig polynomial multiplication | angle has been reduced to the certified core interval | +| HP log/exp multiplication | operands are bounded by the range-reduction contract | +| IV rational initializer | price-like inputs and normalized variables are capped before evaluation | + +Changing one of these domains requires the associated invariant, accuracy, and +overflow tests to change with it. + +## Model contracts + +Each higher-level model exposes the domain it actually implements. A contract +boundary is returned as an error rather than extended by silent extrapolation. + +| Model | Runtime contract | +|---|---| +| Black–Scholes | Positive price, strike, volatility, and time within the public numeric bounds | +| Implied volatility | Bounded solver with `NoConvergence` for prices without a resolvable volatility at `SCALE` | +| American KBI | `0–12%` rates/yields, `10–120%` volatility, `30/365–2` years, and `abs(ln(S/K)) <= 0.75` | +| Exponential NIG | Published alpha/beta, elapsed-scale, rate, time, and moneyness domain plus caller-selected error allowance | +| Arithmetic-Asian/TWAP | Coherent fixed average/weight and averaging times; fixing state supplied by the integration | +| Barrier options | State-aware API accepts the persisted historical breach flag | +| Deterministic Heston | Exact `xi = 0` integrated-variance reduction; other positive-expiry `xi` values return `NoConvergence` | +| SABR | Individual analytics plus an atomic whole-grid certificate for parity, bounds, verticals, butterflies, and calendars | +| Bivariate normal | Analytic endpoint handling and guarded quadrature; unresolved near-singular boundary cases return `NoConvergence` | +| Fixed-correlation Phi2 | Raw analytics lookup or certificate-ID/error-budget-bound evaluation | + +The detailed KBI, NIG, and TWAP contracts are in +[`docs/AMERICAN_KBI.md`](docs/AMERICAN_KBI.md), +[`docs/NIG.md`](docs/NIG.md), and +[`docs/ASIAN_TWAP.md`](docs/ASIAN_TWAP.md). + +## Rounding and settlement + +The crate exposes truncating, nearest, floor, and ceiling variants rather than +hiding an economic rounding choice. `fp_to_token_floor` and +`fp_to_token_ceil` make payout and collection policy explicit at the conversion +boundary. Weighted-pool execution rounds output down and the fee up. + +For barriers and in-progress averages, mathematical parameters are only part of +the contract state. Integrations should derive breach/fixing state from their +persisted accounts or oracle observations before calling the model. + +## Verification + +The release workflow exercises several independent layers: + +- no-default, default, all-feature, MSRV, and embedded `no_std` builds; +- debug and release tests with overflow checks forced both on and off; +- deterministic five-million-iteration checked-input and financial-invariant + sweeps; +- strict production Clippy gates for panic-like constructs; +- thirteen Kani harnesses covering 545 bit-precise checks, including full-width + U256 carry/division bounds, exact square-root Newton/bisection transitions, truncation, + nearest-rounding, overflow, and double-word ULP invariants; +- source-digest-gated Arb/exact-integer certificates for `ln`, `exp`, and the + standard normal CDF; +- high-precision and QuantLib comparison corpora for pricing models; +- deployed-SBF footprint and compute campaigns; +- dependency, package-surface, and secret/history checks. + +The standard `ln` certificate establishes at most 3 ULP over its valid `u128` +domain. The normal-CDF certificate establishes at most 2 ULP, exact symmetry, +and monotonicity for every `i128`. Model-specific empirical and analytical +results are indexed in [VALIDATION.md](VALIDATION.md) and +[PROOFS.md](https://github.com/DJBarker87/solmath/blob/v0.2.0/PROOFS.md). + +## Integration boundary + +SolMath supplies numerical functions; a consuming program supplies the +surrounding protocol controls. Account ownership, signer/PDA authorization, +oracle provenance and freshness, volatility calibration, slippage, transaction +composition, and upgrade policy remain part of that program. + +Measure the exact linked program before choosing compute limits. The repository +figures isolate SolMath calls and benchmark instructions; account +serialization, logs, oracle reads, and CPIs add to the final transaction. + +## Assurance record + +Release evidence is retained as generated corpora, machine-readable reports, +source-bound certificates, deterministic tests, and SBF artifacts. That +reproducible record defines the assurance scope of `0.2.0`; downstream reviews +can extend it to the exact accounts, oracles, and economic rules of an +integrating program. diff --git a/USAGE.md b/USAGE.md index 88ae6ea..8bb9703 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1,7 +1,7 @@ # SolMath — Usage Guide Worked examples for every major runtime feature. All code compiles standalone -with `solmath = { version = "0.1", features = ["full"] }`. Offline table +with `solmath = { version = "0.2", features = ["full"] }`. Offline table generation requires the additional `table-gen` feature. See the generated rustdoc API and the function table in `README.md` for @@ -104,7 +104,7 @@ assert!((ln2 - 693_147_180_560i128).abs() <= 3); // max 3 ULP // exp(1.0) ≈ 2.71828... let e = exp_fixed_i(SCALE_I)?; -assert!((e - 2_718_281_828_459i128).abs() <= 1); // max 1 ULP +assert_eq!(e, 2_718_281_828_459i128); // this input rounds exactly // pow(2.0, 0.5) = sqrt(2) ≈ 1.41421... let root2 = pow_fixed(2 * SCALE, SCALE / 2)?; @@ -154,13 +154,13 @@ assert!((pdf - 398_942_280_401i128).abs() <= 2); // CDF: Φ(0) = 0.5 let cdf = norm_cdf_poly(0)?; -assert!((cdf - SCALE_I / 2).abs() <= 4); +assert!((cdf - SCALE_I / 2).abs() <= 2); // proved for every i128 input // Quantile: Φ⁻¹(0.975) ≈ 1.96 let z = inverse_norm_cdf(975_000_000_000)?; // 0.975 at SCALE assert!((z - 1_959_963_984_540i128).abs() <= 6); -// Combined CDF + PDF (saves ~2K CU vs calling separately) +// Combined CDF + PDF (saves ~338 CU on average vs separate final-SBF calls) let (cdf_val, pdf_val) = norm_cdf_and_pdf(SCALE_I)?; // at x = 1.0 # Ok::<(), SolMathError>(()) ``` @@ -187,17 +187,57 @@ let fast_prob = table.eval(a, b)?; # Ok::<(), SolMathError>(()) ``` -Use `bvn_cdf` for arbitrary correlation. Use `Phi2Table::eval` when a protocol -has a fixed correlation and needs the ~943 CU lookup path. `Phi2Table::generate` -is behind `table-gen` and is intended for native/off-chain table generation. +Use `bvn_cdf` for general correlation. Exact endpoints and certified separated +near-singular limits are analytic; unresolved near-equal `|rho|>.99` inputs +return `NoConvergence`. Use `Phi2Table::eval` only when a protocol accepts its +measured 64x64 bilinear-table error (max 0.001326764088 in the audit corpus). +The final affected-path SBF sample averaged/maxed at 100,498/135,944 CU for +`bvn_cdf`; the broader retained branch-grid audit reached 208,693 CU. Runtime- +backed table lookup averaged/maxed at 1,439/1,440 CU. +`Phi2Table::generate` is offline-only. + +--- + +## Two-asset rainbow options + +The `rainbow` feature prices calls on the minimum or maximum of two assets with +the analytic Stulz formulas: + +```rust +use solmath::{best_of_call, worst_of_call, SCALE}; + +let s1 = 100 * SCALE; +let s2 = 105 * SCALE; +let strike = 100 * SCALE; +let rate = 50_000_000_000; +let q1 = 10_000_000_000; +let q2 = 20_000_000_000; +let sigma1 = 250_000_000_000; +let sigma2 = 300_000_000_000; +let rho = 400_000_000_000i128; +let time = SCALE; + +let worst = worst_of_call( + s1, s2, strike, rate, q1, q2, sigma1, sigma2, rho, time, +)?; +let best = best_of_call( + s1, s2, strike, rate, q1, q2, sigma1, sigma2, rho, time, +)?; + +assert!(best >= worst); +# Ok::<(), solmath::SolMathError>(()) +``` + +Both functions evaluate three bivariate-normal terms on-chain and support +positive or negative return correlation. --- ## Black-Scholes (HP) -The HP path gives 10-14 significant figures in ~118K CU average for prices plus -all 5 Greeks. The standard `bs_full` path is the ~50K CU option when lower -precision is acceptable: +The HP path gives roughly 10-14 significant figures on non-tiny outputs and +averaged 113,177 CU for prices plus all 5 Greeks. The standard `bs_full` path +averaged 24,717 CU and maxed at 25,650 CU when lower precision is acceptable: ```rust use solmath::*; @@ -208,11 +248,11 @@ let r = 50_000_000_000u128; // 5% let sigma = 200_000_000_000u128; // 20% let t = SCALE; // 1 year -// Prices only (~60K CU) +// Prices only (84,528 CU average / 122,380 max) let (call, put) = black_scholes_price_hp(s, k, r, sigma, t)?; // call ≈ $8.02, put ≈ $7.90 -// Full Greeks (~118K CU average, ~165K max in the benchmark set) +// Full Greeks (113,177 CU average / 149,925 max) let g = bs_full_hp(s, k, r, sigma, t)?; // g.call, g.put — option prices // g.call_delta, g.put_delta — deltas (signed) @@ -221,15 +261,79 @@ let g = bs_full_hp(s, k, r, sigma, t)?; // g.call_theta, g.put_theta — thetas // g.call_rho, g.put_rho — rhos -// Individual Greeks at standard precision (~20K CU each) +// Individual Greeks at standard precision (~14K-18K CU average each) let (cd, pd) = bs_delta(s, k, r, sigma, t)?; let gamma = bs_gamma(s, k, r, sigma, t)?; let vega = bs_vega(s, k, r, sigma, t)?; # Ok::<(), SolMathError>(()) ``` -For on-chain use, prefer `bs_full_hp` — it gives production-grade accuracy in a -single instruction. +Use `bs_full_hp` when price and Greek accuracy are the priority; use `bs_full` +when the smaller standard-precision path is sufficient. + +--- + +## American options: Kim Boundary Integration + +Enable `american-kbi` when the early-exercise premium must be computed entirely +inside the program: + +```rust +use solmath::{american_kbi_price, AmericanKbiKind, SCALE}; + +let put = american_kbi_price( + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, // r = 5% + 30_000_000_000, // q = 3% + 300_000_000_000, // sigma = 30% + SCALE, // T = 1 year + AmericanKbiKind::Put, +)?; +# let _ = put; +# Ok::<(), solmath::SolMathError>(()) +``` + +KBI reconstructs the nonlinear smooth-pasting boundary from the live inputs and +then evaluates Kim's early-exercise-premium integral. Its embedded artifact is +parameter-independent geometry plus nine global cubature weights, not a price +table, per-contract coefficient set, or uploaded operator. The +validated domain and QuantLib QdFp results are in +[`docs/AMERICAN_KBI.md`](docs/AMERICAN_KBI.md). + +--- + +## Exponential NIG options + +The `nig` feature provides a European call/put pair under the exponential NIG +model. The result includes the quote-local absolute-error allowance and the +execution tier selected by the engine: + +```rust +use solmath::{nig_price_certified, NigParams, SCALE}; + +let quote = nig_price_certified( + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, // rate = 5% + 20_000_000_000, // dividend yield = 2% + SCALE, // one year + NigParams { + alpha: 15 * SCALE, + beta: -2 * SCALE as i128, + delta_per_year: SCALE, + }, + 5_000_000_000, // requested absolute error = 0.005 +)?; + +// tier 0 = expiry, 1 = Chernoff tail, 15 = Gauss-Kronrod integration +let _ = (quote.call, quote.put, quote.max_abs_error, quote.tier); +# Ok::<(), solmath::SolMathError>(()) +``` + +The martingale correction, scaled-Bessel density, quadrature, and parity step +all execute in the runtime. See [`docs/NIG.md`](docs/NIG.md) for the parameter +domain, reference campaigns, and compute distribution. --- @@ -254,9 +358,9 @@ let sigma_recovered = implied_vol(call, s, k, r, t)?; ``` The solver uses Li rational initial guess + Halley refinement + Jaeckel -rational. Typical cost is ~149K CU median / ~157K CU average; worst cases in -the benchmark set reached ~396K CU, so on-chain callers should request a higher -compute budget for IV. +rational. Accepted final-artifact cases used 82,917 CU median / 88,707 average, +282,132 P99, and 328,660 max. Request at least 500K for the math call plus +surrounding program headroom, and handle `NoConvergence`. --- @@ -286,9 +390,67 @@ let result_in = barrier_option( s, k, h, r, sigma, t, true, BarrierType::DownAndIn, )?; assert_eq!(result.price + result_in.price, result.vanilla); + +// Production contracts must persist historical breach state: +let state_aware = barrier_option_with_state( + s, k, h, r, sigma, t, true, BarrierType::DownAndOut, false, +)?; +# let _ = state_aware; # Ok::<(), SolMathError>(()) ``` +The state-aware unbreached path maxed at 415,531 CU; request at least 500K +before adding account/oracle/CPI work. + +--- + +## Arithmetic-Asian / TWAP settlement + +`twap_option_price` prices a continuously sampled arithmetic average, including +the portion of an in-progress TWAP that is already fixed: + +```rust +use solmath::*; + +let minutes = |n: u128| n * SCALE / (365 * 24 * 60); + +// 12 of 30 settlement minutes have fixed at $99.50; 18 remain. +let result = twap_option_price( + 100 * SCALE, // current spot + 100 * SCALE, // strike + 50_000_000_000, // rate: 5% + 20_000_000_000, // continuous yield: 2% + 600_000_000_000, // volatility: 60% + minutes(18), // time to expiry/payment + minutes(18), // remaining averaging time + 99_500_000_000_000, // fixed average + 400_000_000_000, // fixed weight = 12/30 +)?; + +// result.call / result.put — discounted option marks +// result.expected_average — risk-neutral expected final TWAP +// result.log_variance — variance parameter of the matched lognormal +# Ok::<(), SolMathError>(()) +``` + +Before the averaging window begins, use the full window length with +`fixed_average = fixed_weight = 0`. Once it is completely fixed, use +`averaging_time = 0` and `fixed_weight = SCALE`. Persist the observation +accumulator on-chain and construct [`TwapInputs`] at the instruction boundary; +derive the fixed average and weight from that authenticated observation state. + +The continuous arithmetic moments are exact under constant-parameter GBM, but +the option price uses a two-moment lognormal approximation. See +[`docs/ASIAN_TWAP.md`](docs/ASIAN_TWAP.md) for the equations, validation, and +limitations. The 100K production and 10K adversarial accuracy corpora all +complete without rejection and record their deviations from 60-digit mpmath +references; the adversarial maximum is `$0.000019587949` in a near-deterministic, +near-ATM partially fixed contract. The 2,000-input deployed-SBF sweep measured +137,997 average, 180,029 P99, and 182,458 maximum math CU. A separate +10,000-case adversarial CU sweep maxed at 185,590 math CU and 186,610 CU for the complete benchmark +instruction. Additional account, oracle, logging, or CPI work still requires +explicit budget headroom. + --- ## Weighted pool swap (AMM) @@ -361,9 +523,14 @@ for &strike in &[95 * SCALE, 100 * SCALE, 105 * SCALE, 110 * SCALE] { # Ok::<(), SolMathError>(()) ``` +For a complete executable surface, pass the strike/maturity grid through +`certify_sabr_surface` and consume its typed `CertifiedSabrQuote` nodes. The +accepted guarded price/Greek paths maxed at 603,172/603,160 CU; a 700K math +budget covers those measured paths before surrounding instruction work. + --- -## Heston stochastic volatility +## Deterministic Heston limit ```rust use solmath::*; @@ -375,20 +542,24 @@ let t = SCALE; // 1 year let v0 = 40_000_000_000u128; // initial variance 0.04 (σ₀ = 20%) let kappa = 2 * SCALE; // mean reversion speed let theta = 40_000_000_000u128; // long-run variance 0.04 -let xi = 500_000_000_000u128; // vol of vol 0.50 +let xi = 0u128; // only deterministic variance is executable let rho = -700_000_000_000i128; // spot-vol correlation -0.70 let (call, put) = heston_price(s, k, r, t, v0, kappa, theta, xi, rho)?; -// Accuracy: $0.002-$0.007 typical vs QuantLib -// CU cost: ~410-430K (CV path), ~130K (BS fallback) +// 200,704-case max call/put errors: 200/372 raw SCALE units. +// Final SBF: 118,523 CU average / 183,239 max. # Ok::<(), SolMathError>(()) ``` +This feature implements the deterministic Heston limit. At positive expiry, +`xi > 0` returns `NoConvergence`; stochastic-distribution pricing is available +separately through the exponential NIG engine documented in `docs/NIG.md`. + --- ## Complex arithmetic -Used internally by Heston and NIG, but available for general use: +These primitives are available for general-purpose complex fixed-point work: ```rust use solmath::*; @@ -434,40 +605,58 @@ match ln_fixed_i(0) { Pick features to control binary size. Each feature pulls in its dependencies: ```toml -# Minimal: core arithmetic only (mul, div, sqrt) — +15 KB -solmath = { version = "0.1", default-features = false } +# Minimal: core arithmetic only (mul, div, sqrt) +solmath = { version = "0.2", default-features = false } # AMM pool math (needs transcendental for pow) -solmath = { version = "0.1", default-features = false, features = ["pool"] } +solmath = { version = "0.2", default-features = false, features = ["pool"] } # Options pricing + Greeks -solmath = { version = "0.1", default-features = false, features = ["bs"] } +solmath = { version = "0.2", default-features = false, features = ["bs"] } # Options + implied vol solver -solmath = { version = "0.1", default-features = false, features = ["iv"] } +solmath = { version = "0.2", default-features = false, features = ["iv"] } + +# Arithmetic-Asian / partially fixed TWAP settlement +solmath = { version = "0.2", default-features = false, features = ["asian"] } + +# Fully on-chain American Kim Boundary Integration +solmath = { version = "0.2", default-features = false, features = ["american-kbi"] } + +# Fully on-chain exponential NIG pricing +solmath = { version = "0.2", default-features = false, features = ["nig"] } + +# Two-asset best-of / worst-of options +solmath = { version = "0.2", default-features = false, features = ["rainbow"] } -# Everything -solmath = { version = "0.1", features = ["full"] } +# Every stable runtime module +solmath = { version = "0.2", default-features = false, features = ["full"] } # Bivariate CDF only -solmath = { version = "0.1", default-features = false, features = ["bivariate"] } +solmath = { version = "0.2", default-features = false, features = ["bivariate"] } # Offline Phi2 table generation -solmath = { version = "0.1", default-features = false, features = ["table-gen"] } +solmath = { version = "0.2", default-features = false, features = ["table-gen"] } ``` Feature dependency graph: ``` core (always on) -├── transcendental ← complex ← nig +├── transcendental ← complex │ ← bs ← iv ← pade-iv -│ ← ← heston (also needs complex) +│ ← ← heston │ ← barrier +│ ← asian +│ ← american-kbi +│ ← nig │ ← sabr │ ← pool │ ← bivariate ← table-gen +│ ← rainbow ``` -`full` enables the production runtime features through `bivariate`. It does not -enable `table-gen`, `pade-iv`, or `idl-build`; opt into those explicitly. +`full` enables every stable runtime module, including `american-kbi`, +exponential NIG pricing, two-asset rainbow options, and the deterministic +Heston limit. It does not enable offline `table-gen` or experimental +`pade-iv`; opt into those explicitly. diff --git a/VALIDATION.md b/VALIDATION.md index ba0d211..bbfa26f 100644 --- a/VALIDATION.md +++ b/VALIDATION.md @@ -6,7 +6,7 @@ published crate stays usable while the full audit trail remains reproducible. ## Toolchains -The `0.1.5` release was checked with: +The `0.2.0` release was checked with: - Rust/Cargo release toolchain: `rustc 1.93.0`, `cargo 1.93.0` - MSRV library check: `rustc 1.79.0` @@ -19,16 +19,32 @@ The MSRV job intentionally checks the library surface only: ## Release Checks Run this sequence locally before publishing. It is also the expected CI -sequence if a workflow is added: +sequence enforced by CI: ```bash cargo check --lib --all-features # Rust 1.79.0 MSRV library check git diff --check +cargo fmt --check +cargo clippy --lib --all-features -- -A warnings -D clippy::correctness -D clippy::suspicious -D clippy::perf cargo test --no-default-features cargo test cargo test --all-features +cargo test --release --all-features # repeat with overflow checks on and off +cargo check --target thumbv7em-none-eabihf --lib --all-features +python3 scripts/verify_feature_contract.py cargo check --examples --all-features +./scripts/verify_critical_invariants.sh +python3 scripts/generate_exp_coeffs.py --check +python3 scripts/certify_ln_fixed.py +python3 scripts/certify_exp_fixed.py +python3 scripts/certify_norm_cdf.py +rm -f target/package/solmath-0.2.0.crate cargo package +cargo publish --dry-run +# In the integration harness: anchor build, deploy to local validator, then +# rerun the changed-path CU matrix before updating any published CU figure. +# With Solana's cargo-build-sbf installed, enforce the linked LUT/kernel budget: +./scripts/measure_sbf_footprint.sh ``` ## Local Release Checklist @@ -42,7 +58,9 @@ cargo +stable test --no-default-features cargo +stable test cargo +stable test --all-features cargo +stable check --examples --all-features +rm -f target/package/solmath-0.2.0.crate cargo +stable package +cargo +stable publish --dry-run ``` Then verify the generated package, not only the working tree: @@ -50,8 +68,8 @@ Then verify the generated package, not only the working tree: ```bash rm -rf /tmp/solmath-package mkdir -p /tmp/solmath-package -tar -xzf target/package/solmath-0.1.5.crate -C /tmp/solmath-package -cd /tmp/solmath-package/solmath-0.1.5 +tar -xzf target/package/solmath-0.2.0.crate -C /tmp/solmath-package +cd /tmp/solmath-package/solmath-0.2.0 cargo test --all-features ``` @@ -59,20 +77,118 @@ cargo test --all-features Included in the crates.io package: -- `benchmark/iv_vectors.json` — compact implied-vol recovery regression vectors. - `INTEGRATION.md` — copy-paste Solana integration patterns. -- `test_data/heston_reference_tests.rs` — generated Heston reference tests. -- `test_data/sabr_reference_tests.rs` — generated SABR reference tests. -- `PROOFS.md` — analytical error-bound notes for core approximations. +- `USAGE.md`, `VALIDATION.md`, `SECURITY.md`, `CHANGELOG.md`, architecture, and + the KBI, Asian/TWAP, and NIG guides — usage, release, security, and model + guidance. Repository-only assets: - `scripts/` — Python generators and QuantLib/mpmath cross-check scripts. +- `examples/README.md` and `examples/anchor_options_pricing.md` — + repository/developer guidance not required to build the published crate. +- `benchmark/iv_vectors.json`, `test_data/`, and `tests/` — generated and + integration-test corpora executed in the repository CI/test checkout but not + shipped in the compact crate tarball. +- `benchmark/prod_asian_vectors.json` and `benchmark/adv_asian_vectors.json` — + exact 100K stratified production and 10K seam/tail adversarial arithmetic- + Asian corpora from 60-digit mpmath. `benchmark/asian_accuracy_report.json` + records the compiled 110K sweep. - `tests/reference/mul_div_vectors.json` — large generated mul-div corpus. +- `benchmark/prod_ln_1p_vectors.json` and `benchmark/adv_ln_1p_vectors.json` — + 100K/10K `mpmath.log1p` reference corpora generated on demand. +- `benchmark/prod_expm1_vectors.json` and `benchmark/adv_expm1_vectors.json` — + 100K/10K `mpmath.expm1` full-domain and reduction-seam reference corpora. +- `benchmark/prod_exp_vectors.json` and `benchmark/adv_exp_vectors.json` — exact + 100K production and 10K adversarial exponential corpora. The latter brackets + every ln(2)/32 cell seam and retains the positive-tail amplification zone. +- `benchmark/prod_ln_vectors.json` and `benchmark/adv_ln_vectors.json` — + 100K/10K logarithm corpora spanning every midpoint boundary, every reachable + binary exponent, and `u128` extrema. +- `benchmark/prod_norm_cdf_vectors.json` and + `benchmark/adv_norm_cdf_vectors.json` — 100K/10K CDF corpora covering every + body/tail seam, raw neighborhoods, tail rounding transitions, saturation, + and signed extrema. +- `benchmark/sbf-footprint/` — isolated, locked Anchor harness for comparing + baseline, hybrid, combined, and former calculation-heavy linked SBF sizes. +- `benchmark/sbf-composite/` — locked deployed-SBF harness for bracketing the + complete `ln`/CDF/exp-affected composite call matrix with runtime inputs. +- `benchmark/nig_cu_report.json` and `benchmark/nig_footprint_report.json` — + toolchain-specific NIG deployment measurements retained with their SBF + harnesses in the repository. +- `benchmark/nig_independent_oracle_report.json` — the separate 50-digit NIG + Bessel-density/Lewis cross-check retained alongside its reference script. +- `benchmark/american_kbi_{runtime,unseen}_accuracy_report.json`, + `benchmark/american_kbi_release_report.json`, and + `benchmark/nig_release_report.json` — machine-readable release evidence. The + tagged implementation docs contain their measured summaries; the JSON does + not ship in the crate archive. +- `scripts/generate_american_kbi_data.py` — deterministic generator for KBI's + parameter-independent Q40 quadrature geometry and normal-kernel coefficients. +- `scripts/fit_american_kbi_price_weights.py` — reproduces KBI's nine positive + QdFp-regularized empirical cubature weights from the fixed 48-contract + training corpus; the weights are global integration coefficients, not prices. +- `scripts/validate_american_kbi_runtime.py` and + `benchmark/american_kbi_{runtime,unseen}_accuracy_report.json` — compiled + Rust KBI comparisons against QuantLib QdFp. No generated price is consumed + by the live API. +- `scripts/certify_ln_fixed.py`, `scripts/certify_exp_fixed.py`, and + `scripts/certify_norm_cdf.py` — + source-digest-gated Arb/exact-integer release certificates. All three require + the pinned `python-flint==0.8.0` dependency. The repository-only assets are excluded from the crate tarball to keep install -and docs.rs builds small. The crate test suite still covers mul-div using exact -edge vectors plus deterministic property-style sweeps. +and docs.rs builds small. CI rejects a release archive above 260 KiB. The crate +test suite still covers mul-div using exact edge vectors plus deterministic +property-style sweeps. + +## Publish and provenance gate + +`0.2.0` intentionally contains breaking feature/API changes relative to the +published `0.1.5`. Before publishing, run `cargo semver-checks check-release +--baseline-version 0.1.5 --all-features` and confirm that Cargo derives a +breaking release rather than treating this as a patch. Publish only from a +clean commit whose CI is green: + +```bash +cargo publish --dry-run +git tag -s v0.2.0 -m "solmath 0.2.0" +git push origin v0.2.0 +cargo publish +``` + +Push the signed tag first because documentation inside the crate links to +versioned repository evidence. The tag must point at the exact source commit +used by `cargo publish`. Verify the +crate checksum and docs.rs build after crates.io accepts the release. Registry +MFA, crate ownership, and the signing key are external controls and cannot be +verified from this repository. + +## LUT and Linked-Binary Budgets + +The reduced-domain `ln1p` and `expm1` tables have compile-time raw-payload +limits, so an oversized generated table fails normal compilation. The current +shared payload is 27,944 bytes. Normal CDF has no answer table: its 936 bytes +of coefficient/cutoff data have a separate 2 KiB compile-time cap. Its Q39 +tail evaluation promotes the same stored Q23 coefficients at runtime and adds +no payload. The calculation-first exponential stores 304 bytes of Q22 minimax +coefficients and rounded Q62 fractional power-of-two reconstruction constants +under a 512-byte compile-time cap; it has no sampled-answer table. Linked size is +checked separately because code, helper selection, +alignment, and dead-section elimination cannot be inferred from Rust source +size: + +```bash +./scripts/measure_sbf_footprint.sh +``` + +The harness currently measures linked increases of 19,104 bytes for `expm1`, +25,968 for `ln1p`, 40,952 for both, and 6,976 for `exp_fixed_i`. The final exp +path is 21,272 linked bytes smaller than its former rational implementation. +Its caps allow limited toolchain drift but reject exp growth beyond 10 KiB and +`expm1`/`ln1p`/combined growth beyond 22/34/50 KiB. Any new large lookup +structure requires the same three-way evidence: accuracy corpus, deployed CU +distribution, and linked SBF delta against an identical baseline. To regenerate the larger offline assets from a full repository checkout: @@ -82,26 +198,61 @@ python3 -m venv .venv pip install -r scripts/requirements.txt python3 scripts/generate_production_vectors.py python3 scripts/generate_adversarial_vectors.py +python3 scripts/generate_exp_coeffs.py --check +python3 scripts/generate_norm_cdf_coeffs.py +python3 scripts/certify_ln_fixed.py +python3 scripts/certify_exp_fixed.py +python3 scripts/certify_norm_cdf.py +python3 scripts/generate_bvn_phi2_references.py +python3 scripts/generate_american_kbi_data.py --check src/american_kbi_data.rs python3 scripts/generate_barrier_vectors.py +python3 scripts/validate_asian_runtime.py --cases 500 python3 scripts/crosscheck_quantlib.py ``` -## Production Readiness Matrix - -| Area | Status | Use today? | Notes | -|------|--------|------------|-------| -| Core fixed-point arithmetic | Internally tested, deterministic property-style sweeps | Yes, with protocol review | Overflow returns `Err`, no silent wrapping. | -| Token conversion helpers | Internally tested | Yes, with explicit floor/ceil policy | Use floor for payouts and ceil for collections. | -| Weighted pool math | Internally tested | Candidate | Validate economic invariants for your pool parameters. | -| HP Black-Scholes | QuantLib cross-checked | Candidate | Best-supported pricing path; still unaudited. | -| Standard Black-Scholes | Internally tested | Candidate | Lower precision than HP path. | -| Implied volatility | Roundtrip-tested; offline-vector measured | Caution | Solver returns `NoConvergence`; callers need fallback policy. | -| Barrier options | QuantLib cross-checked | Caution | Requires higher compute budget. | -| Heston, SABR, NIG | Reference-tested | Research/caution | Model risk dominates arithmetic risk; validate assumptions independently. | -| Bivariate CDF/table lookup | mpmath-vector tested | Research/caution | Accuracy degrades near extreme correlations; see README notes. | - -## Audit Status - -No independent third-party audit is claimed. Treat SolMath as unaudited -financial infrastructure until your integration has its own review or an -external audit covers the exact version and feature set you use. +## Capability evidence matrix + +| Capability | Runtime status | Primary evidence | Current measured characteristic | +|---|---|---|---| +| Core fixed-point arithmetic | General runtime API | Deterministic property sweeps and thirteen Kani harnesses | Checked overflow and exact widened mul-div/sqrt fallback; 545/545 bit-precise checks, including U256 limb and sqrt-transition bounds plus `<1` and `<=0.5` ULP rounding lemmas | +| `ln` and normal CDF | All-input source-certified kernels | Arb intervals plus exact-integer monotonicity/symmetry analysis | `ln_fixed_i <= 3` ULP; `norm_cdf_poly <= 2` ULP for every `i128` | +| `exp` | Source-certified runtime kernel | Arb/exact-integer certificate and 100K/10K corpora | Relative error `<1.55e-16`; 961 average / 992 max CU | +| Token conversion and pool math | General runtime APIs | Exact rounding tests and pool-domain invariant sweeps | Explicit floor/ceil conversion; pool quote max 33,685 math CU | +| Black–Scholes and Greeks | Standard and HP runtime paths | 100K high-precision corpus and QuantLib BlackCalculator comparison | HP call/put max error 3/4 raw; 149,925 max CU for all Greeks | +| Implied volatility | Bounded iterative runtime solver | 100K/10K price-to-volatility round trips | 82,917 median / 328,660 max CU on accepted sampled quotes | +| Barrier options | State-aware runtime API | 443,520 QuantLib AnalyticBarrierEngine comparisons | All eight type/side combinations; 415,579 max math CU | +| Arithmetic-Asian / TWAP | Continuous-moment, partially fixed runtime model | 100K/10K mpmath plus 10K QuantLib references | Production max `$2.258e-8`; 182,458 practical max math CU | +| American KBI | Fully on-chain runtime model | Complete 100K/10K comparison plus held-out/unseen QuantLib QdFp surfaces | Production call/put max `$0.002744/$0.003264`; 390,628/390,786 max instruction CU | +| Exponential NIG | Fully on-chain bounded runtime model | 100K/10K CDF-density references and independent 50-digit density/Lewis checks | Max `$0.000565/$0.001397` per `$100`; 382,441 max instruction CU | +| Deterministic Heston (`xi = 0`) | Exact integrated-variance reduction | 200,704-case independent sweep | Call/put max 200/372 raw; 190,756 retained max CU | +| SABR | Analytics plus whole-grid certified execution | QuantLib references and parity/bounds/vertical/butterfly/calendar tests | Stored certified-node reads are 251 CU; largest tested certificate 607,665 CU | +| Bivariate normal and Phi2 tables | Guarded quadrature plus fixed-correlation tables | Independent quadrature corpus and certificate-ID/error-budget tests | `bvn_cdf` 135,944 sampled max CU; table lookup 1,440 max CU | +| Two-asset rainbow options | Analytic Stulz formulas using `bvn_cdf` | Stulz reference and Monte Carlo comparisons | Best-of and worst-of calls for positive/negative correlation | + +The matrix describes the implementation and evidence that ship with `0.2.0`. +Model assumptions and runtime domains are part of each API contract; callers +receive `SolMathError` when a quote cannot be represented by that contract. + +## Evidence index + +The repository retains the full measurement trail rather than embedding large +reports in the crate archive: + +- `.superstack/ln-proof-certificate-2026-07-12.md`, + `.superstack/exp-proof-certificate-2026-07-12.md`, and + `.superstack/norm-cdf-proof-2026-07-14-release.md` bind the source-certified + kernels to their exact implementations. +- `.superstack/kani-arithmetic-verification-2026-07-14.md` records the current + thirteen-harness, 545-check integer proof layer; + `.superstack/kani-ulp-verification-2026-07-14.md` retains the preceding + eight-harness ULP layer and `.superstack/kani-verification-2026-07-12.md` + retains the original two-harness baseline. +- `.superstack/composite-cu-revalidation-2026-07-12.md` and + `.superstack/release-cu-revalidation-2026-07-14.json` retain deployed-SBF CU + measurements. +- `benchmark/american_kbi_*_report.json`, `benchmark/nig_*_report.json`, and + `benchmark/asian_*_report.json` retain the model-specific accuracy, footprint, + and compute results. + +See [SECURITY.md](SECURITY.md) for runtime guarantees and integration +boundaries, and the model guides under `docs/` for equations and domains. diff --git a/benchmark/american_kbi_release_report.json b/benchmark/american_kbi_release_report.json new file mode 100644 index 0000000..14ce7e2 --- /dev/null +++ b/benchmark/american_kbi_release_report.json @@ -0,0 +1,79 @@ +{ + "method": "compiled SolMath KBI versus QuantLib 1.41 QdFp accurateScheme", + "runtime_model": "all six-input-dependent boundary reconstruction and early-exercise-premium integration execute on-chain", + "accuracy_units": "absolute dollars at a $100 strike", + "accuracy_statistics_are_rounded_as_published": true, + "accuracy": { + "production": { + "requested_per_leg": 100000, + "in_domain_and_accepted_per_leg": 78047, + "call": { + "median": 0.0000156, + "p95": 0.0006946, + "p99": 0.0013619, + "maximum": 0.0027441, + "percent_within_0_001": 97.57 + }, + "put": { + "median": 0.0000160, + "p95": 0.0007078, + "p99": 0.0014208, + "maximum": 0.0032638, + "percent_within_0_001": 97.46 + } + }, + "adversarial": { + "requested_per_leg": 10000, + "in_domain_and_accepted_per_leg": 5528, + "call": { + "median": 0.0000234, + "p95": 0.0011672, + "p99": 0.0017998, + "maximum": 0.0029456, + "percent_within_0_001": 92.37 + }, + "put": { + "median": 0.0000279, + "p95": 0.0013621, + "p99": 0.0019915, + "maximum": 0.0027250, + "percent_within_0_001": 91.08 + } + } + }, + "agave_compute_campaign": { + "program_id": "BdR4cSgZGQgXNo33SZSYQXy7XgEK61sHT4NQaAkc3PBm", + "artifact_sha256": "685d49886179ca0bec80e31d9e9b878f3d044a47869cfd2f70cc2cf194c05161", + "artifact_bytes": 1083408, + "quotes_per_leg": 2000, + "call": { + "accepted": 2000, + "average_math_cu": 381096, + "median_math_cu": 382636, + "p95_math_cu": 387937, + "p99_math_cu": 388749, + "maximum_math_cu": 389587, + "maximum_full_instruction_cu": 390628 + }, + "put": { + "accepted": 2000, + "average_math_cu": 371876, + "median_math_cu": 382652, + "p95_math_cu": 387882, + "p99_math_cu": 388879, + "maximum_math_cu": 389742, + "maximum_full_instruction_cu": 390786 + } + }, + "isolated_sbf_footprint": { + "anchor_baseline_bytes": 184848, + "kbi_total_bytes": 293360, + "kbi_linked_delta_bytes": 108512 + }, + "full_feature_composite_after_closed_form_removal": { + "previous_bytes": 1040392, + "current_bytes": 1012056, + "bytes_removed": 28336, + "artifact_sha256": "077c562dfa3f41b9a1f7d05f6bc92ce1bd288897c657aca72929bd0147b24da7" + } +} diff --git a/benchmark/american_kbi_runtime_accuracy_report.json b/benchmark/american_kbi_runtime_accuracy_report.json new file mode 100644 index 0000000..ca5a657 --- /dev/null +++ b/benchmark/american_kbi_runtime_accuracy_report.json @@ -0,0 +1,62 @@ +{ + "method": "compiled Rust Q40 Kim Boundary Integration versus QuantLib QdFp accurateScheme", + "runtime_design": "18-node sqrt-time boundary; six-node singularity-cancelled Gaussian history; nine-node QdFp-regularized empirical premium cubature; log-boundary interpolation; all parameter-dependent work on-chain", + "quantlib_version": "1.41", + "artifact_sha256": "6c0e7857669b9913770de45da32d5cfdeb1d89faf78e7bcdad55d3ee27a218ca", + "contract_source": "deterministic held-out sample seed 0x51d3", + "contract_count": 24, + "moneyness_points_per_contract": 33, + "validation_log_moneyness": [ + -0.75, + 0.75 + ], + "price_comparisons_per_leg": 792, + "absolute_error_dollars_at_100_strike": { + "call": { + "count": 792, + "median": 8.948694253874123e-05, + "p95": 0.0010766623458337676, + "p99": 0.001726966784826303, + "max": 0.0018849224875019388, + "mean": 0.0002856895420760865 + }, + "put": { + "count": 792, + "median": 9.380566574179738e-05, + "p95": 0.0016180610193870845, + "p99": 0.002029547962462673, + "max": 0.0020945350702348264, + "mean": 0.0004238174604345133 + } + }, + "worst_cases": { + "call": { + "contract_index": 23, + "contract": { + "r": 0.030067242360500648, + "q": 0.09809174183039819, + "sigma": 0.7031808649252979, + "days": 685 + }, + "normalized_spot": 0.9542066659691884, + "runtime_price_dollars": 27.279214209272, + "qdfp_price_dollars": 27.2810991317595, + "signed_error_dollars": -0.0018849224875019388, + "absolute_error_dollars": 0.0018849224875019388 + }, + "put": { + "contract_index": 13, + "contract": { + "r": 0.06851114938869349, + "q": 0.1032280972514535, + "sigma": 0.9392856045812212, + "days": 575 + }, + "normalized_spot": 1.7550546569602985, + "runtime_price_dollars": 28.078323718546, + "qdfp_price_dollars": 28.076229183475764, + "signed_error_dollars": 0.0020945350702348264, + "absolute_error_dollars": 0.0020945350702348264 + } + } +} diff --git a/benchmark/american_kbi_unseen_accuracy_report.json b/benchmark/american_kbi_unseen_accuracy_report.json new file mode 100644 index 0000000..d25ac33 --- /dev/null +++ b/benchmark/american_kbi_unseen_accuracy_report.json @@ -0,0 +1,62 @@ +{ + "method": "compiled Rust Q40 Kim Boundary Integration versus QuantLib QdFp accurateScheme", + "runtime_design": "18-node sqrt-time boundary; six-node singularity-cancelled Gaussian history; nine-node QdFp-regularized empirical premium cubature; log-boundary interpolation; all parameter-dependent work on-chain", + "quantlib_version": "1.41", + "artifact_sha256": "6c0e7857669b9913770de45da32d5cfdeb1d89faf78e7bcdad55d3ee27a218ca", + "contract_source": "deterministic sample seed 0xa11ce5eed", + "contract_count": 192, + "moneyness_points_per_contract": 33, + "validation_log_moneyness": [ + -0.75, + 0.75 + ], + "price_comparisons_per_leg": 6336, + "absolute_error_dollars_at_100_strike": { + "call": { + "count": 6336, + "median": 0.00011313755524255953, + "p95": 0.0014037473830699199, + "p99": 0.00205867227602425, + "max": 0.0025022471388744805, + "mean": 0.00033386099441695157 + }, + "put": { + "count": 6336, + "median": 9.742102054843826e-05, + "p95": 0.001304666700314705, + "p99": 0.0019186820970915582, + "max": 0.0026984867177546334, + "mean": 0.0003216052510953452 + } + }, + "worst_cases": { + "call": { + "contract_index": 76, + "contract": { + "r": 0.02219909945230241, + "q": 0.11601668025165202, + "sigma": 0.6924053615222372, + "days": 728 + }, + "normalized_spot": 0.9105103613800342, + "runtime_price_dollars": 23.553534086774, + "qdfp_price_dollars": 23.556036333912875, + "signed_error_dollars": -0.0025022471388744805, + "absolute_error_dollars": 0.0025022471388744805 + }, + "put": { + "contract_index": 55, + "contract": { + "r": 0.049504525515853166, + "q": 0.08653147251806546, + "sigma": 1.1887906150100092, + "days": 694 + }, + "normalized_spot": 2.117000016612675, + "runtime_price_dollars": 41.008256781788, + "qdfp_price_dollars": 41.005558295070244, + "signed_error_dollars": 0.0026984867177546334, + "absolute_error_dollars": 0.0026984867177546334 + } + } +} diff --git a/benchmark/asian_accuracy_report.json b/benchmark/asian_accuracy_report.json new file mode 100644 index 0000000..2213c8b --- /dev/null +++ b/benchmark/asian_accuracy_report.json @@ -0,0 +1,326 @@ +{ + "binary": "target/release/examples/asian_batch", + "corpora": [ + { + "accepted": 100000, + "category_max_raw": { + "fully_fixed": { + "expected_call": 1, + "expected_log_variance": 0, + "expected_mean": 0, + "expected_put": 2 + }, + "partial_high": { + "expected_call": 22580, + "expected_log_variance": 11, + "expected_mean": 24, + "expected_put": 22580 + }, + "partial_low": { + "expected_call": 12110, + "expected_log_variance": 38, + "expected_mean": 41, + "expected_put": 12109 + }, + "unseasoned": { + "expected_call": 10324, + "expected_log_variance": 25, + "expected_mean": 55, + "expected_put": 10324 + } + }, + "error_count": 0, + "errors": [], + "file": "benchmark/prod_asian_vectors.json", + "max_cases": { + "expected_call": { + "actual_raw": 1292516855198, + "category": "partial_high", + "difference_raw": 22580, + "expected_raw": 1292516832618, + "index": 73998, + "inputs": { + "averaging_time": "27155779124", + "fixed_average": "490348271125054", + "fixed_weight": "783463001134", + "k": "491004255994972", + "q": "41549074800", + "r": "19922217555", + "s": "498985480275581", + "sigma": "95483712338", + "t": "33551608118" + } + }, + "expected_log_variance": { + "actual_raw": 452877733553, + "category": "partial_low", + "difference_raw": 38, + "expected_raw": 452877733515, + "index": 18617, + "inputs": { + "averaging_time": "604035796124", + "fixed_average": "145782329176569", + "fixed_weight": "245309620828", + "k": "130680253310946", + "q": "16059513348", + "r": "15823130492", + "s": "137792200619658", + "sigma": "788653101821", + "t": "1538394964355" + } + }, + "expected_mean": { + "actual_raw": 498670997660764, + "category": "unseasoned", + "difference_raw": 55, + "expected_raw": 498670997660819, + "index": 42548, + "inputs": { + "averaging_time": "219516793607", + "fixed_average": "0", + "fixed_weight": "0", + "k": "406741035971315", + "q": "24849590364", + "r": "71746527580", + "s": "472431746119126", + "sigma": "120405307691", + "t": "1262261278853" + } + }, + "expected_put": { + "actual_raw": 125668667907, + "category": "partial_high", + "difference_raw": 22580, + "expected_raw": 125668645327, + "index": 73998, + "inputs": { + "averaging_time": "27155779124", + "fixed_average": "490348271125054", + "fixed_weight": "783463001134", + "k": "491004255994972", + "q": "41549074800", + "r": "19922217555", + "s": "498985480275581", + "sigma": "95483712338", + "t": "33551608118" + } + } + }, + "metrics": { + "call": { + "max_raw": 22580, + "max_real": 2.258e-08, + "median_raw": 29, + "p95_raw": 634, + "p99_raw": 1180 + }, + "log_variance": { + "max_raw": 38, + "max_real": 3.8e-11, + "median_raw": 0, + "p95_raw": 1, + "p99_raw": 1 + }, + "mean": { + "max_raw": 55, + "max_real": 5.5e-11, + "median_raw": 0, + "p95_raw": 5, + "p99_raw": 14 + }, + "put": { + "max_raw": 22580, + "max_real": 2.258e-08, + "median_raw": 29, + "p95_raw": 633, + "p99_raw": 1180 + } + }, + "reference": "mpmath 1.4.1, 60 decimal digits, independent continuous-GBM moment match", + "vectors": 100000 + }, + { + "accepted": 10000, + "category_max_raw": { + "carry_raw_seam": { + "expected_call": 3949, + "expected_log_variance": 2, + "expected_mean": 1, + "expected_put": 3950 + }, + "deep_tail_low_vol": { + "expected_call": 79, + "expected_log_variance": 2, + "expected_mean": 87, + "expected_put": 91 + }, + "fixing_weight_seam": { + "expected_call": 2920, + "expected_log_variance": 7, + "expected_mean": 82, + "expected_put": 2921 + }, + "fully_fixed": { + "expected_call": 1, + "expected_log_variance": 0, + "expected_mean": 0, + "expected_put": 2 + }, + "future_start": { + "expected_call": 3086, + "expected_log_variance": 2, + "expected_mean": 4, + "expected_put": 3086 + }, + "high_variance": { + "expected_call": 3965, + "expected_log_variance": 50, + "expected_mean": 77, + "expected_put": 3966 + }, + "partial_fixing_cdf_sensitivity": { + "expected_call": 19587949, + "expected_log_variance": 1, + "expected_mean": 1, + "expected_put": 19587949 + }, + "series_above": { + "expected_call": 8518, + "expected_log_variance": 2, + "expected_mean": 80, + "expected_put": 8517 + }, + "series_below": { + "expected_call": 4577, + "expected_log_variance": 1, + "expected_mean": 61, + "expected_put": 4599 + }, + "tiny_window": { + "expected_call": 3406, + "expected_log_variance": 1, + "expected_mean": 2, + "expected_put": 3406 + }, + "zero_carry": { + "expected_call": 3573, + "expected_log_variance": 1, + "expected_mean": 0, + "expected_put": 3573 + } + }, + "error_count": 0, + "errors": [], + "file": "benchmark/adv_asian_vectors.json", + "max_cases": { + "expected_call": { + "actual_raw": 2660563740, + "category": "partial_fixing_cdf_sensitivity", + "difference_raw": 19587949, + "expected_raw": 2640975791, + "index": 8278, + "inputs": { + "averaging_time": "10541470976", + "fixed_average": "953502354017465", + "fixed_weight": "997626820179", + "k": "953327576600744", + "q": "50527678659", + "r": "11734747999", + "s": "880867502150846", + "sigma": "27259966587", + "t": "11377974106" + } + }, + "expected_log_variance": { + "actual_raw": 14003948971929, + "category": "high_variance", + "difference_raw": 50, + "expected_raw": 14003948971979, + "index": 7894, + "inputs": { + "averaging_time": "606760923477", + "fixed_average": "255839965359635", + "fixed_weight": "314649615796", + "k": "486750025554448", + "q": "38592240777", + "r": "39077716176", + "s": "336330286976840", + "sigma": "5146857321883", + "t": "734757683618" + } + }, + "expected_mean": { + "actual_raw": 910423895608520, + "category": "deep_tail_low_vol", + "difference_raw": 87, + "expected_raw": 910423895608433, + "index": 6633, + "inputs": { + "averaging_time": "228149398874", + "fixed_average": "0", + "fixed_weight": "0", + "k": "6908170690080383", + "q": "168725954542", + "r": "119810853400", + "s": "915513487609572", + "sigma": "30715248600", + "t": "228149398874" + } + }, + "expected_put": { + "actual_raw": 754159326, + "category": "partial_fixing_cdf_sensitivity", + "difference_raw": 19587949, + "expected_raw": 734571377, + "index": 8278, + "inputs": { + "averaging_time": "10541470976", + "fixed_average": "953502354017465", + "fixed_weight": "997626820179", + "k": "953327576600744", + "q": "50527678659", + "r": "11734747999", + "s": "880867502150846", + "sigma": "27259966587", + "t": "11377974106" + } + } + }, + "metrics": { + "call": { + "max_raw": 19587949, + "max_real": 1.9587949e-05, + "median_raw": 129, + "p95_raw": 67666, + "p99_raw": 1220288 + }, + "log_variance": { + "max_raw": 50, + "max_real": 5e-11, + "median_raw": 0, + "p95_raw": 1, + "p99_raw": 2 + }, + "mean": { + "max_raw": 87, + "max_real": 8.7e-11, + "median_raw": 0, + "p95_raw": 12, + "p99_raw": 31 + }, + "put": { + "max_raw": 19587949, + "max_real": 1.9587949e-05, + "median_raw": 129, + "p95_raw": 67667, + "p99_raw": 1220288 + } + }, + "reference": "mpmath 1.4.1, 60 decimal digits, independent continuous-GBM moment match", + "vectors": 10000 + } + ], + "scale": 1000000000000, + "total_errors": 0, + "total_vectors": 110000 +} diff --git a/benchmark/asian_cu_report.json b/benchmark/asian_cu_report.json new file mode 100644 index 0000000..3d31030 --- /dev/null +++ b/benchmark/asian_cu_report.json @@ -0,0 +1,50 @@ +{ + "run_date": "2026-07-13T12:46:29.534Z", + "target": "Agave local validator at http://127.0.0.1:8899", + "measurement": "math CU is the difference between sol_log_compute_units markers; transaction CU is meta.computeUnitsConsumed for the complete Anchor instruction", + "program_id": "95cap8Cwznz3PHc3bUMc3Tp28ghySZktkGEWkNDuUGgu", + "artifact": "sha256:b7c40646067194b8b071e90adefd0bb6f009e4960c901d44e15ff718addcdaa4", + "artifact_bytes": 1446912, + "target_max_cu": 200000, + "overall_math_max": 185590, + "overall_transaction_max": 186610, + "headroom_to_target": 13390, + "results": [ + { + "name": "arithmetic_asian_price", + "corpus": "2,000 deterministic practical-domain runtime inputs", + "total": 2000, + "measured": 2000, + "accepted": 2000, + "rejected": 0, + "errors": 0, + "min": 88268, + "average": 137997, + "median": 161471, + "p95": 177422, + "p99": 180029, + "max": 182458, + "transaction_average": 139017, + "transaction_p99": 181049, + "transaction_max": 183478 + }, + { + "name": "arithmetic_asian_price_adversarial", + "corpus": "10,000 full-domain and branch-seam runtime inputs; rejected rows are expected SolMathError returns and remain CU-metered", + "total": 10000, + "measured": 10000, + "accepted": 6668, + "rejected": 3332, + "errors": 0, + "min": 272, + "average": 68124, + "median": 79849, + "p95": 166579, + "p99": 175987, + "max": 185590, + "transaction_average": 69145, + "transaction_p99": 177007, + "transaction_max": 186610 + } + ] +} diff --git a/benchmark/nig_cu_report.json b/benchmark/nig_cu_report.json new file mode 100644 index 0000000..a6d9dee --- /dev/null +++ b/benchmark/nig_cu_report.json @@ -0,0 +1,35 @@ +{ + "run_date": "2026-07-14T16:39:29.991Z", + "target": "Agave local validator at http://127.0.0.1:8899", + "measurement": "unsigned simulateTransaction; math CU is the difference between sol_log_compute_units markers and full CU is unitsConsumed", + "program_id": "BdR4cSgZGQgXNo33SZSYQXy7XgEK61sHT4NQaAkc3PBm", + "artifact": "benchmark/sbf-composite/target/deploy/solmath_sbf_composite.so", + "artifact_sha256": "685d49886179ca0bec80e31d9e9b878f3d044a47869cfd2f70cc2cf194c05161", + "artifact_bytes": 1083408, + "limit_per_suite": 2000, + "concurrency": 32, + "results": [ + { + "name": "nig_price_certified", + "total": 2000, + "measured": 2000, + "accepted": 1787, + "rejected": 213, + "errors": 0, + "min": 23692, + "average": 129872, + "median": 28261, + "p95": 347885, + "p99": 367321, + "max": 381385, + "accepted_average": 110009, + "accepted_median": 28035, + "accepted_p95": 350643, + "accepted_p99": 367931, + "accepted_max": 381385, + "transaction_average": 130929, + "transaction_p99": 368377, + "transaction_max": 382441 + } + ] +} diff --git a/benchmark/nig_footprint_report.json b/benchmark/nig_footprint_report.json new file mode 100644 index 0000000..36e5d3e --- /dev/null +++ b/benchmark/nig_footprint_report.json @@ -0,0 +1,20 @@ +{ + "run_date": "2026-07-14", + "toolchain": "Solana CLI / Agave 2.3.0 cargo-build-sbf, fat LTO, one codegen unit, overflow checks enabled", + "baseline": { + "bytes": 184848, + "sha256": "04bd66556683a8b98e760b61655fb27cdef7fe182bcc3aafde892c56cd133c33" + }, + "nig": { + "bytes": 311464, + "sha256": "8ffd259c6b28570f26ed5ea7aec90891582c78f8b06c7ae14242e5d9ad2fbed0" + }, + "linked_delta_bytes": 126616, + "linked_delta_kib": 123.6484375, + "full_composite": { + "bytes": 1083408, + "sha256": "685d49886179ca0bec80e31d9e9b878f3d044a47869cfd2f70cc2cf194c05161", + "historical_fail_closed_bytes": 1012056, + "delta_vs_historical_fail_closed_bytes": 71352 + } +} diff --git a/benchmark/nig_independent_oracle_report.json b/benchmark/nig_independent_oracle_report.json new file mode 100644 index 0000000..831d7f8 --- /dev/null +++ b/benchmark/nig_independent_oracle_report.json @@ -0,0 +1,162 @@ +{ + "schema": 1, + "mpmath_dps": 50, + "methods": [ + "direct NIG Bessel-density OTM integration, arbitrary-precision Gauss-Legendre 64", + "Lewis characteristic-function inversion", + "SolMath fixed-point 15/7 runtime" + ], + "cases": 6, + "maxima": { + "density_48_vs_64": 1.7883668668810862e-19, + "density_vs_fourier": 1.7748655069058008e-16, + "fixed_vs_density": 0.00010331520640711848, + "fixed_vs_fourier": 0.00010331520640729597 + }, + "rows": [ + { + "index": 0, + "input": { + "spot": 100, + "strike": 100, + "rate": 0.05, + "dividend": 0.02, + "time": 1.0, + "alpha": 10, + "beta": -2, + "delta_per_year": 0.2 + }, + "density_call": "6.8920151084221355700270514219024782250288386270652", + "density_put": "3.9950902278180062570881729773368076607663435141881", + "fourier_call": "6.8920151084221355700270488713168390555398246164381", + "fourier_put": "3.9950902278180062570881704267511684912773295035611", + "fixed_call": 6.892015135701, + "fixed_put": 3.995090255101, + "returned_max_abs_error": 0.004430974051, + "tier": 15, + "density_48_vs_64": 9.447724496736135e-21, + "density_vs_fourier": 2.550585639169489e-24, + "fixed_vs_density": 2.7282993742911827e-08 + }, + { + "index": 1, + "input": { + "spot": 80, + "strike": 100, + "rate": 0.03, + "dividend": 0.01, + "time": 0.5, + "alpha": 8, + "beta": -3, + "delta_per_year": 0.4 + }, + "density_call": "0.53365302776000992264982159739853772842879201418305", + "density_put": "19.443848652651691001973515081152727178543511808289", + "fourier_call": "0.53365302776000992264982307899230068243123433218625", + "fourier_put": "19.443848652651691001973516562746490132545954126292", + "fixed_call": 0.533653034901, + "fixed_put": 19.443848659761, + "returned_max_abs_error": 0.000270636015, + "tier": 15, + "density_48_vs_64": 1.1928230473054685e-24, + "density_vs_fourier": 1.4815937629540025e-24, + "fixed_vs_density": 7.140990077350179e-09 + }, + { + "index": 2, + "input": { + "spot": 130, + "strike": 100, + "rate": -0.01, + "dividend": 0.04, + "time": 2.0, + "alpha": 12, + "beta": 2, + "delta_per_year": 0.3 + }, + "density_call": "21.337043419354108682102638449981014533606471719479", + "density_put": "3.3520523917670379197182500538856127582915726298808", + "fourier_call": "21.337043419354108682102319780695463064350925118001", + "fourier_put": "3.3520523917670379197179313846000612890360260284021", + "fixed_call": 21.33704244104, + "fixed_put": 3.35205141343, + "returned_max_abs_error": 0.010270426921, + "tier": 15, + "density_48_vs_64": 1.1545773295478984e-25, + "density_vs_fourier": 3.1866928555146923e-22, + "fixed_vs_density": 9.783370379197183e-07 + }, + { + "index": 3, + "input": { + "spot": 100, + "strike": 115, + "rate": -0.04, + "dividend": 0.08, + "time": 1.5, + "alpha": 2.5, + "beta": -0.8, + "delta_per_year": 0.25 + }, + "density_call": "4.0445025932893422379774143414776144886496672248567", + "density_put": "37.463661774289947241059652338856697029990757347089", + "fourier_call": "4.0445025932893422379774154931902829056766520947095", + "fourier_put": "37.463661774289947241059653490569365447017742216942", + "fixed_call": 4.044502611918, + "fixed_put": 37.463661792893, + "returned_max_abs_error": 0.000244222405, + "tier": 15, + "density_48_vs_64": 1.3515748478538718e-20, + "density_vs_fourier": 1.151712668417027e-24, + "fixed_vs_density": 1.8628657762022587e-08 + }, + { + "index": 4, + "input": { + "spot": 100, + "strike": 85, + "rate": 0.12, + "dividend": -0.03, + "time": 0.75, + "alpha": 4, + "beta": 1, + "delta_per_year": 0.5 + }, + "density_call": "27.143952334704046095686354196537125864905700879064", + "density_put": "2.5525996663138498868419629192062671830119048181144", + "fourier_call": "27.143952334704046095686353649664982714670706497846", + "fourier_put": "2.5525996663138498868419623723341240327769104368972", + "fixed_call": 27.143952334275, + "fixed_put": 2.55259966591, + "returned_max_abs_error": 0.001245207739, + "tier": 15, + "density_48_vs_64": 1.367229729890781e-23, + "density_vs_fourier": 5.46872143150235e-25, + "fixed_vs_density": 4.290460956863542e-10 + }, + { + "index": 5, + "input": { + "spot": 250, + "strike": 250, + "rate": 0.0, + "dividend": 0.0, + "time": 0.25, + "alpha": 100, + "beta": -20, + "delta_per_year": 1.0 + }, + "density_call": "5.1068124184564071184842733928556616578085524499969", + "density_put": "5.106812418456407118484273392855661657808552449997", + "fourier_call": "5.1068124184564072959708240834357365329389529073699", + "fourier_put": "5.1068124184564072959708240834357365329389529073699", + "fixed_call": 5.10670910325, + "fixed_put": 5.10670910325, + "returned_max_abs_error": 0.547724832096, + "tier": 15, + "density_48_vs_64": 1.7883668668810862e-19, + "density_vs_fourier": 1.7748655069058008e-16, + "fixed_vs_density": 0.00010331520640711848 + } + ] +} diff --git a/benchmark/nig_release_report.json b/benchmark/nig_release_report.json new file mode 100644 index 0000000..e0bfb41 --- /dev/null +++ b/benchmark/nig_release_report.json @@ -0,0 +1,129 @@ +{ + "schema": 1, + "model": "exponential NIG with martingale correction and beta+1 Esscher shift", + "runtime": "SolMath fixed-point direct OTM Gauss-Kronrod 15/7", + "reference": "SciPy norminvgauss CDF Esscher identity; upper tails by NIG reflection", + "versions": { + "python": "3.12.0", + "numpy": "2.4.2", + "scipy": "1.17.0" + }, + "scale": 1000000000000, + "seed": 86076636995622, + "production": { + "name": "production", + "seeded_input_sha256": "61cb4bd21a99928fde3fd0355d33ef48678bb45e4de342b7e1b534c70fdb097c", + "quotes": 100000, + "accepted": 87715, + "acceptance_rate": 0.87715, + "rejected": 12285, + "reference_failures": 0, + "reference_fallbacks": 0, + "reject_reasons": { + "ERR:NoConvergence": 12285 + }, + "tiers": { + "1": 31930, + "15": 55785 + }, + "certificate_violations": 0, + "request_violations": 0, + "absolute_error": { + "median": 2.028841498188837e-09, + "p90": 1.0762443544198934e-06, + "p99": 3.6945983244720546e-05, + "p999": 0.0001742850570218451, + "max": 0.0021548693882778025 + }, + "absolute_error_per_100_notional": { + "median": 4.021595665651977e-09, + "p90": 1.6352131242004103e-06, + "p99": 1.7308069350477835e-05, + "p999": 4.08408994775416e-05, + "max": 0.0005652776645896646 + }, + "error_over_returned_allowance": { + "p99": 0.010272889300898749, + "max": 0.5718392025867882 + }, + "worst_accepted_quote": { + "index": 23145, + "input": { + "spot": 656239072868277, + "strike": 2748672559598486, + "rate": 157701107916, + "dividend": -241722846734, + "time": 4682096661123, + "alpha": 96024659535280, + "beta": 61169458419724, + "delta": 2411550747456, + "requested": 137433627980 + }, + "runtime_call": 808.35060600567, + "runtime_put": 86.828370210432, + "reference_call": 808.3527608750583, + "reference_put": 86.8305250784249, + "returned_max_abs_error": 0.020350891321, + "max_call_put_abs_error": 0.0021548693882778025, + "tier": 15 + } + }, + "adversarial": { + "name": "adversarial", + "seeded_input_sha256": "8853de248219801b23eca772f3613caf011d1f8b0408ddd877645bb0c35e9340", + "quotes": 10000, + "accepted": 6898, + "acceptance_rate": 0.6898, + "rejected": 3102, + "reference_failures": 0, + "reference_fallbacks": 0, + "reject_reasons": { + "ERR:NoConvergence": 3102 + }, + "tiers": { + "1": 2539, + "15": 4359 + }, + "certificate_violations": 0, + "request_violations": 0, + "absolute_error": { + "median": 9.38094989594285e-10, + "p90": 4.297227797422926e-06, + "p99": 0.0006958166536893379, + "p999": 0.013403356502481015, + "max": 0.5708145623193559 + }, + "absolute_error_per_100_notional": { + "median": 5.307431714912183e-09, + "p90": 1.6350595724560344e-06, + "p99": 0.00017026511177001385, + "p999": 0.0011384939968817015, + "max": 0.00139654993281919 + }, + "error_over_returned_allowance": { + "p99": 0.017329896572244013, + "max": 0.2374114404934143 + }, + "worst_accepted_quote": { + "index": 1112, + "input": { + "spot": 8646523765152555, + "strike": 63889622966698520, + "rate": 200000000000, + "dividend": -200000000000, + "time": 4999999000000, + "alpha": 3000000000000, + "beta": 949999974331, + "delta": 1622937091, + "requested": 6388962296670 + }, + "runtime_call": 263.596781490237, + "runtime_put": 263.596546429515, + "reference_call": 264.16759602867023, + "reference_put": 264.16736099183436, + "returned_max_abs_error": 3.199794483487, + "max_call_put_abs_error": 0.5708145623193559, + "tier": 15 + } + } +} diff --git a/benchmark/sbf-composite/Cargo.lock b/benchmark/sbf-composite/Cargo.lock new file mode 100644 index 0000000..3a1f57c --- /dev/null +++ b/benchmark/sbf-composite/Cargo.lock @@ -0,0 +1,1632 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anchor-attribute-access-control" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a883ca44ef14b2113615fc6d3a85fefc68b5002034e88db37f7f1f802f88aa9" +dependencies = [ + "anchor-syn", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-account" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c4d97763b29030412b4b80715076377edc9cc63bc3c9e667297778384b9fd2" +dependencies = [ + "anchor-syn", + "bs58", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-constant" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae3328bbf9bbd517a51621b1ba6cbec06cbbc25e8cfc7403bddf69bcf088206" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-error" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2398a6d9e16df1ee9d7d37d970a8246756de898c8dd16ef6bdbe4da20cf39a" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-event" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12758f4ec2f0e98d4d56916c6fe95cb23d74b8723dd902c762c5ef46ebe7b65" +dependencies = [ + "anchor-syn", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-program" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7193b5af2649813584aae6e3569c46fd59616a96af2083c556b13136c3830f" +dependencies = [ + "anchor-lang-idl", + "anchor-syn", + "anyhow", + "bs58", + "heck", + "proc-macro2", + "quote", + "serde_json", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-accounts" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d332d1a13c0fca1a446de140b656e66110a5e8406977dcb6a41e5d6f323760b0" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-serde" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8656e4af182edaeae665fa2d2d7ee81148518b5bd0be9a67f2a381bb17da7d46" +dependencies = [ + "anchor-syn", + "borsh-derive-internal", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-space" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcff2a083560cd79817db07d89a4de39a2c4b2eaa00c1742cf0df49b25ff2bed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-lang" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67d85d5376578f12d840c29ff323190f6eecd65b00a0b5f2b2f232751d049cc" +dependencies = [ + "anchor-attribute-access-control", + "anchor-attribute-account", + "anchor-attribute-constant", + "anchor-attribute-error", + "anchor-attribute-event", + "anchor-attribute-program", + "anchor-derive-accounts", + "anchor-derive-serde", + "anchor-derive-space", + "anchor-lang-idl", + "base64 0.21.7", + "bincode", + "borsh 0.10.4", + "bytemuck", + "solana-account-info", + "solana-clock", + "solana-cpi", + "solana-define-syscall", + "solana-feature-gate-interface", + "solana-instruction", + "solana-instructions-sysvar", + "solana-invoke", + "solana-loader-v3-interface", + "solana-msg", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", + "solana-sysvar", + "solana-sysvar-id", + "thiserror", +] + +[[package]] +name = "anchor-lang-idl" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47914b4290ae2bdf4ec203aa821e6eba86d7c78ef497918938038dcc6919f953" +dependencies = [ + "anchor-lang-idl-spec", + "anyhow", + "heck", + "regex", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "anchor-lang-idl-spec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bdf143115440fe621bdac3a29a1f7472e09f6cd82b2aa569429a0c13f103838" +dependencies = [ + "anyhow", + "serde", +] + +[[package]] +name = "anchor-syn" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93b69aa7d099b59378433f6d7e20e1008fc10c69e48b220270e5b3f2ec4c8be" +dependencies = [ + "anyhow", + "bs58", + "cargo_toml", + "heck", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "syn 1.0.109", + "thiserror", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115e54d64eb62cdebad391c19efc9dce4981c690c85a33a12199d99bb9546fee" +dependencies = [ + "borsh-derive 0.10.4", + "hashbrown 0.13.2", +] + +[[package]] +name = "borsh" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" +dependencies = [ + "borsh-derive 1.7.0", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831213f80d9423998dd696e2c5345aba6be7a0bd8cd19e31c5243e13df1cef89" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" +dependencies = [ + "once_cell", + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65d6ba50644c98714aa2a70d13d7df3cd75cd2b523a2b452bf010443800976b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276691d96f063427be83e6692b86148e488ebba9f48f77788724ca027ba3b6d4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] + +[[package]] +name = "bytemuck" +version = "1.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cargo_toml" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a98356df42a2eb1bd8f1793ae4ee4de48e384dd974ce5eac8eee802edb7492be" +dependencies = [ + "serde", + "toml 0.8.23", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rand_core", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "five8" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75b8549488b4715defcb0d8a8a1c1c76a80661b5fa106b4ca0e7fce59d7d875" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_const" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26dec3da8bc3ef08f2c04f61eab298c3ab334523e55f076354d6d6f613799a7b" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2551bf44bc5f776c15044b9b94153a00198be06743e262afaaa61f11ac7523a5" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml 0.5.11", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.5+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "solana-account-info" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8f5152a288ef1912300fc6efa6c2d1f9bb55d9398eb6c72326360b8063987da" +dependencies = [ + "solana-program-error", + "solana-program-memory", + "solana-pubkey", +] + +[[package]] +name = "solana-atomic-u64" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52e52720efe60465b052b9e7445a01c17550666beec855cce66f44766697bc2" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "solana-clock" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8584296123df8fe229b95e2ebfd37ae637fe9db9b7d4dd677ac5a78e80dbfce" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-cpi" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc71126edddc2ba014622fc32d0f5e2e78ec6c5a1e0eb511b85618c09e9ea11" +dependencies = [ + "solana-account-info", + "solana-define-syscall", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-stable-layout", +] + +[[package]] +name = "solana-decode-error" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c781686a18db2f942e70913f7ca15dc120ec38dcab42ff7557db2c70c625a35" +dependencies = [ + "num-traits", +] + +[[package]] +name = "solana-define-syscall" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae3e2abcf541c8122eafe9a625d4d194b4023c20adde1e251f94e056bb1aee2" + +[[package]] +name = "solana-epoch-rewards" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b575d3dd323b9ea10bb6fe89bf6bf93e249b215ba8ed7f68f1a3633f384db7" +dependencies = [ + "serde", + "serde_derive", + "solana-hash", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-epoch-schedule" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fce071fbddecc55d727b1d7ed16a629afe4f6e4c217bc8d00af3b785f6f67ed" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-feature-gate-interface" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f5c5382b449e8e4e3016fb05e418c53d57782d8b5c30aa372fc265654b956d" +dependencies = [ + "solana-pubkey", + "solana-sdk-ids", +] + +[[package]] +name = "solana-fee-calculator" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89bc408da0fb3812bc3008189d148b4d3e08252c79ad810b245482a3f70cd8d" +dependencies = [ + "log", + "serde", + "serde_derive", +] + +[[package]] +name = "solana-hash" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b96e9f0300fa287b545613f007dfe20043d7812bee255f418c1eb649c93b63" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "five8", + "js-sys", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-sanitize", + "wasm-bindgen", +] + +[[package]] +name = "solana-instruction" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab5682934bd1f65f8d2c16f21cb532526fcc1a09f796e2cacdb091eee5774ad" +dependencies = [ + "bincode", + "getrandom", + "js-sys", + "num-traits", + "serde", + "serde_derive", + "serde_json", + "solana-define-syscall", + "solana-pubkey", + "wasm-bindgen", +] + +[[package]] +name = "solana-instructions-sysvar" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0e85a6fad5c2d0c4f5b91d34b8ca47118fc593af706e523cdbedf846a954f57" +dependencies = [ + "bitflags", + "solana-account-info", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-sanitize", + "solana-sdk-ids", + "solana-serialize-utils", + "solana-sysvar-id", +] + +[[package]] +name = "solana-invoke" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f5693c6de226b3626658377168b0184e94e8292ff16e3d31d4766e65627565" +dependencies = [ + "solana-account-info", + "solana-define-syscall", + "solana-instruction", + "solana-program-entrypoint", + "solana-stable-layout", +] + +[[package]] +name = "solana-last-restart-slot" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6360ac2fdc72e7463565cd256eedcf10d7ef0c28a1249d261ec168c1b55cdd" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-loader-v3-interface" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4be76cfa9afd84ca2f35ebc09f0da0f0092935ccdac0595d98447f259538c2" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", +] + +[[package]] +name = "solana-msg" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36a1a14399afaabc2781a1db09cb14ee4cc4ee5c7a5a3cfcc601811379a8092" +dependencies = [ + "solana-define-syscall", +] + +[[package]] +name = "solana-program-entrypoint" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ce041b1a0ed275290a5008ee1a4a6c48f5054c8a3d78d313c08958a06aedbd" +dependencies = [ + "solana-account-info", + "solana-msg", + "solana-program-error", + "solana-pubkey", +] + +[[package]] +name = "solana-program-error" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee2e0217d642e2ea4bee237f37bd61bb02aec60da3647c48ff88f6556ade775" +dependencies = [ + "borsh 1.7.0", + "num-traits", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-pubkey", +] + +[[package]] +name = "solana-program-memory" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a5426090c6f3fd6cfdc10685322fede9ca8e5af43cd6a59e98bfe4e91671712" +dependencies = [ + "solana-define-syscall", +] + +[[package]] +name = "solana-program-option" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc677a2e9bc616eda6dbdab834d463372b92848b2bfe4a1ed4e4b4adba3397d0" + +[[package]] +name = "solana-program-pack" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "319f0ef15e6e12dc37c597faccb7d62525a509fec5f6975ecb9419efddeb277b" +dependencies = [ + "solana-program-error", +] + +[[package]] +name = "solana-pubkey" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b62adb9c3261a052ca1f999398c388f1daf558a1b492f60a6d9e64857db4ff1" +dependencies = [ + "borsh 0.10.4", + "borsh 1.7.0", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "five8", + "five8_const", + "getrandom", + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-decode-error", + "solana-define-syscall", + "solana-sanitize", + "solana-sha256-hasher", + "wasm-bindgen", +] + +[[package]] +name = "solana-rent" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1aea8fdea9de98ca6e8c2da5827707fb3842833521b528a713810ca685d2480" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sanitize" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f1bc1357b8188d9c4a3af3fc55276e56987265eb7ad073ae6f8180ee54cecf" + +[[package]] +name = "solana-sdk-ids" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5d8b9cc68d5c88b062a33e23a6466722467dde0035152d8fb1afbcdf350a5f" +dependencies = [ + "solana-pubkey", +] + +[[package]] +name = "solana-sdk-macro" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86280da8b99d03560f6ab5aca9de2e38805681df34e0bb8f238e69b29433b9df" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "solana-serialize-utils" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "817a284b63197d2b27afdba829c5ab34231da4a9b4e763466a003c40ca4f535e" +dependencies = [ + "solana-instruction", + "solana-pubkey", + "solana-sanitize", +] + +[[package]] +name = "solana-sha256-hasher" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa3feb32c28765f6aa1ce8f3feac30936f16c5c3f7eb73d63a5b8f6f8ecdc44" +dependencies = [ + "sha2", + "solana-define-syscall", + "solana-hash", +] + +[[package]] +name = "solana-slot-hashes" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8691982114513763e88d04094c9caa0376b867a29577939011331134c301ce" +dependencies = [ + "serde", + "serde_derive", + "solana-hash", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-slot-history" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ccc1b2067ca22754d5283afb2b0126d61eae734fc616d23871b0943b0d935e" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-stable-layout" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f14f7d02af8f2bc1b5efeeae71bc1c2b7f0f65cd75bcc7d8180f2c762a57f54" +dependencies = [ + "solana-instruction", + "solana-pubkey", +] + +[[package]] +name = "solana-stake-interface" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5269e89fde216b4d7e1d1739cf5303f8398a1ff372a81232abbee80e554a838c" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-clock", + "solana-cpi", + "solana-decode-error", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-system-interface", + "solana-sysvar-id", +] + +[[package]] +name = "solana-system-interface" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d7c18cb1a91c6be5f5a8ac9276a1d7c737e39a21beba9ea710ab4b9c63bc90" +dependencies = [ + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-decode-error", + "solana-instruction", + "solana-pubkey", + "wasm-bindgen", +] + +[[package]] +name = "solana-sysvar" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c3595f95069f3d90f275bb9bd235a1973c4d059028b0a7f81baca2703815db" +dependencies = [ + "base64 0.22.1", + "bincode", + "lazy_static", + "serde", + "serde_derive", + "solana-account-info", + "solana-clock", + "solana-define-syscall", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-hash", + "solana-instruction", + "solana-instructions-sysvar", + "solana-last-restart-slot", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-pubkey", + "solana-rent", + "solana-sanitize", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-slot-hashes", + "solana-slot-history", + "solana-stake-interface", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sysvar-id" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5762b273d3325b047cfda250787f8d796d781746860d5d0a746ee29f3e8812c1" +dependencies = [ + "solana-pubkey", + "solana-sdk-ids", +] + +[[package]] +name = "solmath" +version = "0.2.0" + +[[package]] +name = "solmath-sbf-composite" +version = "0.0.0" +dependencies = [ + "anchor-lang", + "indexmap", + "solana-msg", + "solmath", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.0.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" +dependencies = [ + "indexmap", + "toml_datetime 1.0.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.0.10+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "zerocopy" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/benchmark/sbf-composite/Cargo.toml b/benchmark/sbf-composite/Cargo.toml new file mode 100644 index 0000000..fd0d78d --- /dev/null +++ b/benchmark/sbf-composite/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "solmath-sbf-composite" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib", "lib"] +name = "solmath_sbf_composite" + +[features] +default = [] +cpi = ["no-entrypoint"] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +custom-heap = [] +custom-panic = [] +anchor-debug = [] +idl-build = ["anchor-lang/idl-build"] + +[dependencies] +anchor-lang = "=0.32.1" +indexmap = "=2.13.0" +solana-msg = "=2.2.1" +solmath = { path = "../..", default-features = false, features = ["full"] } + +[profile.release] +overflow-checks = true +lto = "fat" +codegen-units = 1 diff --git a/benchmark/sbf-composite/measure.mjs b/benchmark/sbf-composite/measure.mjs new file mode 100644 index 0000000..e6131ab --- /dev/null +++ b/benchmark/sbf-composite/measure.mjs @@ -0,0 +1,501 @@ +#!/usr/bin/env node +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; + +const require = createRequire(import.meta.url); +const web3 = require("@solana/web3.js"); +const { + ComputeBudgetProgram, + Connection, + Keypair, + PublicKey, + Transaction, + TransactionInstruction, + sendAndConfirmTransaction, +} = web3; + +const SCRIPT_ROOT = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(SCRIPT_ROOT, "../.."); +const VECTOR_ROOT = process.env.SOLMATH_VECTOR_ROOT || path.join(ROOT, "benchmark"); +const TEST_DATA_ROOT = process.env.SOLMATH_TEST_DATA_ROOT || path.join(ROOT, "test_data"); +const RPC = process.env.ANCHOR_PROVIDER_URL || "http://127.0.0.1:8899"; +const WALLET = process.env.ANCHOR_WALLET || path.join(os.homedir(), ".config/solana/id.json"); + +function positiveInteger(name, fallback) { + const value = Number.parseInt(process.env[name] || fallback, 10); + if (!Number.isSafeInteger(value) || value <= 0) { + throw new Error(`${name} must be a positive integer`); + } + return value; +} + +const LIMIT = positiveInteger("BENCH_LIMIT", "2000"); +const CONCURRENCY = positiveInteger("BENCH_CONCURRENCY", "32"); +const SIMULATE_ONLY = process.env.BENCH_SIMULATE_ONLY === "1"; +const OUTPUT_PATH = path.resolve( + ROOT, + process.env.BENCH_OUTPUT || ".superstack/composite-cu-revalidation-2026-07-12.json", +); +const ARTIFACT = process.env.SBF_ARTIFACT || null; +const ARTIFACT_PATH = ARTIFACT === null ? null : path.resolve(ROOT, ARTIFACT); +const PROGRAM_ID = new PublicKey( + process.env.SBF_PROGRAM_ID || "BdR4cSgZGQgXNo33SZSYQXy7XgEK61sHT4NQaAkc3PBm", +); + +const connection = new Connection(RPC, "confirmed"); +const payer = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(fs.readFileSync(WALLET, "utf8")))); + +function discriminator(name) { + return crypto.createHash("sha256").update(`global:${name}`).digest().subarray(0, 8); +} + +function unsignedLE(value, bytes) { + let v = BigInt(value); + if (v < 0n) throw new Error(`negative unsigned value ${value}`); + const out = Buffer.alloc(bytes); + for (let i = 0; i < bytes; i += 1) { + out[i] = Number(v & 255n); + v >>= 8n; + } + if (v !== 0n) throw new Error(`value ${value} does not fit u${bytes * 8}`); + return out; +} + +function signedLE(value, bytes) { + let v = BigInt(value); + const modulus = 1n << BigInt(bytes * 8); + if (v < 0n) v += modulus; + return unsignedLE(v, bytes); +} + +const U128 = (value) => unsignedLE(value, 16); +const I128 = (value) => signedLE(value, 16); +const I64 = (value) => signedLE(value, 8); + +function instruction(name, encodedArgs) { + return new TransactionInstruction({ + programId: PROGRAM_ID, + keys: [], + data: Buffer.concat([discriminator(name), ...encodedArgs]), + }); +} + +function load(filename, directory = VECTOR_ROOT) { + return () => { + const raw = JSON.parse(fs.readFileSync(path.join(directory, filename), "utf8")); + return Array.isArray(raw) ? raw : raw.vectors; + }; +} + +function stratified(values, count = LIMIT) { + if (values.length <= count) return values; + return Array.from({ length: count }, (_, index) => values[Math.floor(index * values.length / count)]); +} + +function percentile(sorted, percent) { + if (sorted.length === 0) return 0; + return sorted[Math.min(sorted.length - 1, Math.ceil(percent / 100 * sorted.length) - 1)]; +} + +function summarize(name, rows) { + const measured = rows.filter((row) => Number.isInteger(row.cu)); + const values = measured.map((row) => row.cu).sort((a, b) => a - b); + const transactionValues = measured + .map((row) => row.transactionCu) + .filter(Number.isInteger) + .sort((a, b) => a - b); + const accepted = measured.filter((row) => row.succeeded).length; + const acceptedValues = measured + .filter((row) => row.succeeded) + .map((row) => row.cu) + .sort((a, b) => a - b); + const average = values.length === 0 ? 0 : Math.round(values.reduce((sum, value) => sum + value, 0) / values.length); + const acceptedAverage = acceptedValues.length === 0 + ? 0 + : Math.round(acceptedValues.reduce((sum, value) => sum + value, 0) / acceptedValues.length); + const transactionAverage = transactionValues.length === 0 + ? 0 + : Math.round(transactionValues.reduce((sum, value) => sum + value, 0) / transactionValues.length); + return { + name, + total: rows.length, + measured: measured.length, + accepted, + rejected: measured.length - accepted, + errors: rows.length - measured.length, + min: values[0] || 0, + average, + median: percentile(values, 50), + p95: percentile(values, 95), + p99: percentile(values, 99), + max: values.at(-1) || 0, + accepted_average: acceptedAverage, + accepted_median: percentile(acceptedValues, 50), + accepted_p95: percentile(acceptedValues, 95), + accepted_p99: percentile(acceptedValues, 99), + accepted_max: acceptedValues.at(-1) || 0, + transaction_average: transactionAverage, + transaction_p99: percentile(transactionValues, 99), + transaction_max: transactionValues.at(-1) || 0, + }; +} + +async function getTransaction(signature) { + for (let attempt = 0; attempt < 10; attempt += 1) { + const transaction = await connection.getTransaction(signature, { + commitment: "confirmed", + maxSupportedTransactionVersion: 0, + }); + if (transaction) return transaction; + await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1))); + } + return null; +} + +async function measureOne(ix) { + for (let attempt = 0; attempt < 4; attempt += 1) { + try { + const transaction = new Transaction().add( + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }), + ix, + ); + if (SIMULATE_ONLY) { + transaction.feePayer = payer.publicKey; + const simulation = await connection.simulateTransaction(transaction); + const logs = simulation.value.logs || []; + const remaining = logs.flatMap((line) => { + const match = line.match(/consumption:\s*(\d+)\s*units remaining/); + return match ? [Number.parseInt(match[1], 10)] : []; + }); + const successLine = logs.find((line) => line.includes("succeeded =")); + return { + signature: null, + cu: remaining.length >= 2 ? remaining[0] - remaining[1] : null, + transactionCu: Number(simulation.value.unitsConsumed ?? NaN), + succeeded: successLine?.includes("true") || false, + error: simulation.value.err === null && remaining.length >= 2 + ? null + : JSON.stringify(simulation.value.err) || "missing CU markers", + }; + } + const signature = await sendAndConfirmTransaction(connection, transaction, [payer], { + commitment: "confirmed", + skipPreflight: false, + }); + const details = await getTransaction(signature); + const logs = details?.meta?.logMessages || []; + const remaining = logs.flatMap((line) => { + const match = line.match(/consumption:\s*(\d+)\s*units remaining/); + return match ? [Number.parseInt(match[1], 10)] : []; + }); + const successLine = logs.find((line) => line.includes("succeeded =")); + return { + signature, + cu: remaining.length >= 2 ? remaining[0] - remaining[1] : null, + transactionCu: Number(details?.meta?.computeUnitsConsumed ?? NaN), + succeeded: successLine?.includes("true") || false, + error: remaining.length >= 2 ? null : "missing CU markers", + }; + } catch (error) { + if (attempt === 3) return { cu: null, succeeded: false, error: String(error) }; + await new Promise((resolve) => setTimeout(resolve, 250 * (attempt + 1))); + } + } + return { cu: null, succeeded: false, error: "unreachable" }; +} + +async function runSuite(name, vectorSource, encode) { + const vectors = typeof vectorSource === "function" ? vectorSource() : vectorSource; + const sampled = stratified(vectors); + const rows = []; + for (let offset = 0; offset < sampled.length; offset += CONCURRENCY) { + const batch = sampled.slice(offset, offset + CONCURRENCY); + rows.push(...await Promise.all(batch.map((value) => measureOne(encode(value))))); + if ((offset + batch.length) % 500 === 0 || offset + batch.length === sampled.length) { + process.stdout.write(`${name}: ${offset + batch.length}/${sampled.length}\n`); + } + } + const result = summarize(name, rows); + const sampleError = rows.find((row) => row.error)?.error; + if (sampleError) process.stdout.write(`${name} sample error: ${sampleError}\n`); + process.stdout.write(`${JSON.stringify(result)}\n`); + return { result, rows }; +} + +function bvnVectors() { + let state = 0x9e3779b9n; + const next = () => { + state = (1664525n * state + 1013904223n) & 0xffffffffn; + return Number(state) / 0x100000000; + }; + return Array.from({ length: Math.max(LIMIT, 2000) }, (_, index) => ({ + a: BigInt(Math.round((-4 + 8 * next()) * 1e12)).toString(), + b: BigInt(Math.round((-4 + 8 * next()) * 1e12)).toString(), + rho: index % 20 === 0 + ? (index % 40 === 0 ? "990000000000" : "-990000000000") + : BigInt(Math.round((-0.95 + 1.9 * next()) * 1e12)).toString(), + })); +} + +function americanKbiVectors() { + const strike = 100_000_000_000_000n; + let state = 0xa11ce55n; + const next = () => { + state = (1664525n * state + 1013904223n) & 0xffffffffn; + return Number(state) / 0x100000000; + }; + return Array.from({ length: Math.max(LIMIT, 2000) }, (_, index) => { + const logMoneyness = -0.75 + 1.5 * index / (Math.max(LIMIT, 2000) - 1); + const rate = index % 41 === 0 ? 0 : 0.12 * next(); + const dividendYield = index % 43 === 0 ? 0.12 : 0.12 * next(); + const sigma = index % 47 === 0 ? 0.10 : (index % 53 === 0 ? 1.20 : 0.10 + 1.10 * next()); + const maturity = index % 59 === 0 ? 30 / 365 : (index % 61 === 0 ? 2 : 30 / 365 + (2 - 30 / 365) * next()); + return { + spot: BigInt(Math.round(Number(strike) * Math.exp(logMoneyness))).toString(), + strike: strike.toString(), + rate: BigInt(Math.round(rate * 1e12)).toString(), + dividendYield: BigInt(Math.round(dividendYield * 1e12)).toString(), + sigma: BigInt(Math.round(sigma * 1e12)).toString(), + maturity: BigInt(Math.round(maturity * 1e12)).toString(), + }; + }); +} + +function nigRuntimeVectors() { + let state = 0x4e494720n; + const next = () => { + state = (1664525n * state + 1013904223n) & 0xffffffffn; + return Number(state) / 0x100000000; + }; + const raw = (value) => BigInt(Math.round(value * 1e12)).toString(); + return Array.from({ length: Math.max(LIMIT, 2000) }, (_, index) => { + const time = 0.01 + 4.99 * next(); + const alpha = 2.05 + 97.95 * next(); + const low = Math.max(-0.64 * alpha, -1 - 0.64 * alpha); + const high = Math.min(0.64 * alpha, -1 + 0.64 * alpha); + const beta = low + (high - low) * next(); + const minDelta = 0.00101 / time; + const maxDelta = Math.min(14.99, 14.99 / time); + const delta = minDelta * Math.pow(maxDelta / minDelta, next()); + const rate = -0.249 + 0.498 * next(); + const dividend = -0.249 + 0.498 * next(); + const logForward = -1.99 + 3.98 * next(); + const spot = 100; + const strike = spot / Math.exp(logForward - (rate - dividend) * time); + const notional = Math.max(spot, strike); + return { + s: raw(spot), + k: raw(strike), + r: raw(rate), + q: raw(dividend), + t: raw(time), + alpha: raw(alpha), + beta: raw(beta), + delta: raw(delta), + requested: raw(notional * 5e-5), + index, + }; + }); +} + +function asianVectors() { + let state = 0xa51a2026n; + const next = () => { + state = (1664525n * state + 1013904223n) & 0xffffffffn; + return Number(state) / 0x100000000; + }; + const raw = (value) => BigInt(Math.round(value * 1e12)).toString(); + return Array.from({ length: Math.max(LIMIT, 2000) }, (_, index) => { + const spot = 20 + 480 * next(); + const time = 1 / 365 + (2 - 1 / 365) * next(); + const averagingTime = index % 5 === 0 + ? Math.min(time, 30 / (365 * 24 * 60)) + : Math.max(1 / (365 * 24), time * next()); + const fixedWeight = index % 3 === 0 ? 0 : 0.01 + 0.94 * next(); + return { + s: raw(spot), + k: raw(spot * (0.5 + next())), + r: raw(0.2 * next()), + q: raw(0.2 * next()), + sigma: raw(0.05 + 1.95 * next()), + t: raw(time), + averagingTime: raw(averagingTime), + fixedAverage: fixedWeight === 0 ? "0" : raw(spot * (0.7 + 0.6 * next())), + fixedWeight: raw(fixedWeight), + }; + }); +} + +function asianAdversarialVectors() { + let state = 0x41534941n; + const next = () => { + state = (1664525n * state + 1013904223n) & 0xffffffffn; + return Number(state) / 0x100000000; + }; + const raw = (value) => BigInt(Math.max(1, Math.round(value * 1e12))).toString(); + const count = Math.max(LIMIT, 2000); + return Array.from({ length: count }, (_, index) => { + const mode = index % 12; + const spot = 0.001 + 99_999.999 * next(); + const strike = 0.001 + 99_999.999 * next(); + let rate = 10 * next(); + let q = 10 * next(); + let sigma = 0.000001 + 99.999999 * next(); + let time = 0.000001 + 99.999999 * next(); + let averagingTime = time * (0.000001 + 0.999999 * next()); + let fixedWeight = next(); + + if (mode === 0) { + // One-raw-unit maturity/window: smallest structurally valid contract. + time = 1e-12; averagingTime = time; sigma = 0.05; rate = 0; q = 0; + } else if (mode === 1) { + // Minute-scale in-progress TWAP with almost all observations fixed. + time = 1 / (365 * 24 * 60); averagingTime = time; sigma = 2; + rate = 0.2; q = 0.2; fixedWeight = 0.999999999999; + } else if (mode === 2) { + // Exercise the degree-8 series immediately below its |A|+|B| seam. + sigma = 0.7; rate = 0.11; q = 0.03; + averagingTime = 0.249999 / (sigma * sigma + 2 * Math.abs(rate - q)); + time = averagingTime; + } else if (mode === 3) { + // Exercise the closed form immediately above the same seam. + sigma = 0.7; rate = 0.11; q = 0.03; + averagingTime = 0.250001 / (sigma * sigma + 2 * Math.abs(rate - q)); + time = averagingTime; + } else if (mode === 4) { + // Long future start followed by a very short averaging window. + sigma = 0.6; rate = 0.08; q = 0.02; time = 50; + averagingTime = 30 / (365 * 24 * 60); fixedWeight = 0; + } else if (mode === 5) { + // Zero-carry second-moment branch. + rate = 0.12; q = 0.12; sigma = 1.2; time = 2; + averagingTime = 2; + } else if (mode === 6) { + // Tiny non-zero carry on each side of zero. + rate = index % 24 === 6 ? 0.050000000001 : 0.05; + q = index % 24 === 6 ? 0.05 : 0.050000000001; + // V > 0.25 forces the dedicated small-carry expansion rather than the + // origin bivariate series. + sigma = 1.9; time = 0.5; averagingTime = 0.1; + } else if (mode === 7) { + // Near the accepted HP exponential boundary. + rate = 0.39; q = 0; sigma = 0.01; time = 99.9; + averagingTime = 0.01; + } else if (mode === 8) { + // Deep normal-CDF tail and small variance. + sigma = 0.000001; rate = 0.01; q = 0; time = 1; + averagingTime = 1; fixedWeight = 0; + } else if (mode === 9) { + // Broad high-volatility path, often expected to fail closed quickly. + sigma = 100; rate = 10; q = 0; time = 100; averagingTime = 100; + } else if (mode === 10) { + // Fully fixed discounted-intrinsic fast path. + fixedWeight = 1; averagingTime = 0; sigma = 1; + } else { + // Ordinary partial fixing with future weight still material. + sigma = 0.05 + 1.95 * next(); rate = 0.2 * next(); q = 0.2 * next(); + time = 1 / 365 + (2 - 1 / 365) * next(); + averagingTime = time * (0.001 + 0.999 * next()); + fixedWeight = 0.01 + 0.98 * next(); + } + + const fullyFixed = fixedWeight >= 1; + const noFixedPart = !fullyFixed && fixedWeight === 0; + return { + s: raw(spot), + k: raw(strike), + r: raw(rate), + q: raw(q), + sigma: raw(sigma), + t: raw(time), + averagingTime: fullyFixed ? "0" : raw(Math.min(time, averagingTime)), + fixedAverage: noFixedPart ? "0" : raw(0.001 + 99_999.999 * next()), + fixedWeight: fullyFixed ? "1000000000000" : raw(Math.min(fixedWeight, 0.999999999999)), + }; + }); +} + +const suites = [ + ["ln_fixed_i", load("prod_ln_vectors.json"), (v) => instruction("compute_ln", [U128(v.x)])], + ["exp_fixed_i", load("prod_exp_vectors.json"), (v) => instruction("compute_exp", [I128(v.x)])], + ["norm_cdf_poly", load("prod_norm_cdf_vectors.json"), (v) => instruction("compute_cdf", [I128(v.x)])], + ["ln_fixed_hp", load("prod_ln_vectors.json"), (v) => instruction("compute_ln_hp", [I128(BigInt(v.x) * 1000n)])], + ["exp_fixed_hp", load("prod_exp_vectors.json"), (v) => instruction("compute_exp_hp", [I128(BigInt(v.x) * 1000n)])], + ["norm_cdf_poly_hp", load("prod_norm_cdf_vectors.json"), (v) => instruction("compute_cdf_hp", [I128(BigInt(v.x) * 1000n)])], + ["fp_div_hp_safe", load("prod_norm_cdf_vectors.json"), (v) => instruction("compute_div_hp", [I128(BigInt(v.x) * 1000n + 2_000_000_000_000_000n), I128(3_000_000_000_000_000n)])], + ["norm_pdf", load("prod_norm_pdf_vectors.json"), (v) => instruction("compute_pdf", [I128(v.x)])], + ["pow_fixed", load("prod_pow_fixed_vectors.json"), (v) => instruction("compute_pow", [U128(v.base), U128(v.exp)])], + ["pow_fixed_i", load("prod_pow_fixed_i_vectors.json"), (v) => instruction("compute_pow_i", [I128(v.base), I128(v.exp)])], + ["pow_int", load("prod_pow_int_vectors.json"), (v) => instruction("compute_pow_int", [U128(v.base), U128(v.n)])], + ["inverse_norm_cdf", load("prod_inverse_norm_cdf_vectors.json"), (v) => instruction("compute_inverse_cdf", [I128(v.p)])], + ["norm_cdf_and_pdf", load("prod_cdf_pdf_vectors.json"), (v) => instruction("compute_cdf_pdf", [I128(v.x)])], + ["norm_cdf_and_pdf_poly", load("prod_cdf_pdf_vectors.json"), (v) => instruction("compute_cdf_pdf_poly", [I128(v.x)])], + ["fp_mul_i_round", bvnVectors(), (v) => instruction("compute_mul_i_round", [I128(v.a), I128(v.b)])], + ["fp_mul_i_fast", bvnVectors(), (v) => instruction("compute_mul_i_fast", [I128(v.a), I128(v.b)])], + ["fp_mul_i_fast_round", bvnVectors(), (v) => instruction("compute_mul_i_fast_round", [I128(v.a), I128(v.b)])], + ["fp_div_i", bvnVectors(), (v) => instruction("compute_div_i", [I128(v.a), I128(v.b === "0" ? "1000000000000" : v.b)])], + ["black_scholes_price", load("prod_black_scholes_price_vectors.json"), (v) => instruction("compute_bs_price", [U128(v.s), U128(v.k), U128(v.r), U128(v.sigma), U128(v.t)])], + ["bs_full", load("prod_bs_full_vectors.json"), (v) => instruction("compute_bs_full", [U128(v.s), U128(v.k), U128(v.r), U128(v.sigma), U128(v.t)])], + ["bs_delta", load("prod_greeks_vectors.json"), (v) => instruction("compute_bs_delta", [U128(v.s), U128(v.k), U128(v.r), U128(v.sigma), U128(v.t)])], + ["bs_gamma", load("prod_greeks_vectors.json"), (v) => instruction("compute_bs_gamma", [U128(v.s), U128(v.k), U128(v.r), U128(v.sigma), U128(v.t)])], + ["bs_vega", load("prod_greeks_vectors.json"), (v) => instruction("compute_bs_vega", [U128(v.s), U128(v.k), U128(v.r), U128(v.sigma), U128(v.t)])], + ["bs_theta", load("prod_greeks_vectors.json"), (v) => instruction("compute_bs_theta", [U128(v.s), U128(v.k), U128(v.r), U128(v.sigma), U128(v.t)])], + ["bs_rho", load("prod_greeks_vectors.json"), (v) => instruction("compute_bs_rho", [U128(v.s), U128(v.k), U128(v.r), U128(v.sigma), U128(v.t)])], + ["implied_vol", load("prod_implied_vol_vectors.json"), (v) => instruction("compute_iv", [U128(v.call_price), U128(v.s), U128(v.k), U128(v.r), U128(v.t)])], + ["arithmetic_asian_price", asianVectors(), (v) => instruction("compute_asian", [U128(v.s), U128(v.k), U128(v.r), U128(v.q), U128(v.sigma), U128(v.t), U128(v.averagingTime), U128(v.fixedAverage), U128(v.fixedWeight)])], + ["arithmetic_asian_price_adversarial", asianAdversarialVectors(), (v) => instruction("compute_asian", [U128(v.s), U128(v.k), U128(v.r), U128(v.q), U128(v.sigma), U128(v.t), U128(v.averagingTime), U128(v.fixedAverage), U128(v.fixedWeight)])], + ["sabr_implied_vol", load("sabr_vectors.json", TEST_DATA_ROOT), (v) => instruction("compute_sabr_vol", [U128(v.F_fp), U128(v.K_fp), U128(v.T_fp), U128(v.alpha_fp), U128(v.beta_fp), I128(v.rho_fp), U128(v.nu_fp)])], + ["sabr_price", load("sabr_vectors.json", TEST_DATA_ROOT), (v) => instruction("compute_sabr_price", [U128(v.F_fp), U128(v.K_fp), U128(0), U128(v.T_fp), U128(v.alpha_fp), U128(v.beta_fp), I128(v.rho_fp), U128(v.nu_fp)])], + ["sabr_greeks", load("sabr_vectors.json", TEST_DATA_ROOT), (v) => instruction("compute_sabr_greeks", [U128(v.F_fp), U128(v.K_fp), U128(0), U128(v.T_fp), U128(v.alpha_fp), U128(v.beta_fp), I128(v.rho_fp), U128(v.nu_fp)])], + ["sabr_precompute_and_vol_at", load("sabr_vectors.json", TEST_DATA_ROOT), (v) => instruction("compute_sabr_precomputed_vol", [U128(v.F_fp), U128(v.K_fp), U128(v.T_fp), U128(v.alpha_fp), U128(v.beta_fp), I128(v.rho_fp), U128(v.nu_fp)])], + ["sabr_precompute", load("sabr_vectors.json", TEST_DATA_ROOT), (v) => instruction("compute_sabr_precompute", [U128(v.F_fp), U128(v.T_fp), U128(v.alpha_fp), U128(v.beta_fp), I128(v.rho_fp), U128(v.nu_fp)])], + ["sabr_vol_at", load("sabr_vectors.json", TEST_DATA_ROOT), (v) => instruction("compute_sabr_vol_at", [U128(v.F_fp), U128(v.K_fp), U128(v.T_fp), U128(v.alpha_fp), U128(v.beta_fp), I128(v.rho_fp), U128(v.nu_fp)])], + ["sabr_z_over_chi_pade", load("prod_sabr_z_over_chi_vectors.json"), (v) => instruction("compute_sabr_z_over_chi", [I128(v.z), I128(v.rho)])], + ["bvn_cdf", bvnVectors(), (v) => instruction("compute_bvn", [I128(v.a), I128(v.b), I128(v.rho)])], + ["bvn_cdf_hp", bvnVectors(), (v) => instruction("compute_bvn_hp", [I128(v.a), I128(v.b), I128(v.rho)])], + ["heston_deterministic", () => load("heston_vectors.json", TEST_DATA_ROOT)().map((v) => ({ ...v, xi_fp: "0" })), (v) => instruction("compute_heston", [U128(v.S_fp), U128(v.K_fp), U128(v.r_fp), U128(v.T_fp), U128(v.v0_fp), U128(v.kappa_fp), U128(v.theta_fp), U128(v.xi_fp), I128(v.rho_fp)])], + ["heston_stochastic_reject", load("heston_vectors.json", TEST_DATA_ROOT), (v) => instruction("compute_heston", [U128(v.S_fp), U128(v.K_fp), U128(v.r_fp), U128(v.T_fp), U128(v.v0_fp), U128(v.kappa_fp), U128(v.theta_fp), U128(v.xi_fp), I128(v.rho_fp)])], + ["nig_i128_reject", load("nig_call_price_scale_vectors.json"), (v) => instruction("compute_nig", [U128(v.s), U128(v.k), U128(v.r), U128(v.t), U128(v.alpha), I128(v.beta), U128(v.delta)])], + ["nig_price_certified", nigRuntimeVectors(), (v) => instruction("compute_nig_certified", [U128(v.s), U128(v.k), I128(v.r), I128(v.q), U128(v.t), U128(v.alpha), I128(v.beta), U128(v.delta), U128(v.requested)])], + ["nig_i64_call_reject", load("nig_cos_vectors.json"), (v) => instruction("compute_nig_64", [I64(v.s), I64(v.k), I64(v.r), I64(v.t), I64(v.alpha), I64(v.beta), I64(v.delta_param)])], + ["nig_i64_put_reject", load("nig_cos_vectors.json"), (v) => instruction("compute_nig_put_64", [I64(v.s), I64(v.k), I64(v.r), I64(v.t), I64(v.alpha), I64(v.beta), I64(v.delta_param)])], + ["Phi2Table.eval", bvnVectors(), (v) => instruction("compute_phi2_eval", [I128(v.a), I128(v.b)])], + ["american_kbi_call", americanKbiVectors(), (v) => instruction("compute_american_kbi_call", [U128(v.spot), U128(v.strike), U128(v.rate), U128(v.dividendYield), U128(v.sigma), U128(v.maturity)])], + ["american_kbi_put", americanKbiVectors(), (v) => instruction("compute_american_kbi_put", [U128(v.spot), U128(v.strike), U128(v.rate), U128(v.dividendYield), U128(v.sigma), U128(v.maturity)])], +]; + +const output = { + run_date: new Date().toISOString(), + target: process.env.BENCH_TARGET || `Agave local validator at ${RPC}`, + measurement: SIMULATE_ONLY + ? "unsigned simulateTransaction; math CU is the difference between sol_log_compute_units markers and full CU is unitsConsumed" + : "signed transaction; math CU is the difference between sol_log_compute_units markers and full CU is unitsConsumed", + program_id: PROGRAM_ID.toBase58(), + artifact: ARTIFACT, + artifact_sha256: ARTIFACT_PATH === null + ? null + : crypto.createHash("sha256").update(fs.readFileSync(ARTIFACT_PATH)).digest("hex"), + artifact_bytes: ARTIFACT_PATH === null ? null : fs.statSync(ARTIFACT_PATH).size, + limit_per_suite: LIMIT, + concurrency: CONCURRENCY, + results: [], +}; + +const requestedSuites = new Set( + (process.env.BENCH_SUITES || "").split(",").map((name) => name.trim()).filter(Boolean), +); +const selectedSuites = requestedSuites.size === 0 + ? suites + : suites.filter(([name]) => requestedSuites.has(name)); + +for (const [name, vectors, encode] of selectedSuites) { + const { result } = await runSuite(name, vectors, encode); + output.results.push(result); + fs.writeFileSync(OUTPUT_PATH, `${JSON.stringify(output, null, 2)}\n`); +} + +process.stdout.write(`Wrote ${OUTPUT_PATH}\n`); diff --git a/benchmark/sbf-composite/package-lock.json b/benchmark/sbf-composite/package-lock.json new file mode 100644 index 0000000..6b3f3df --- /dev/null +++ b/benchmark/sbf-composite/package-lock.json @@ -0,0 +1,1046 @@ +{ + "name": "solmath-sbf-composite-benchmark", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "solmath-sbf-composite-benchmark", + "version": "0.0.0", + "dependencies": { + "@solana/web3.js": "1.98.4" + }, + "engines": { + "node": ">=20.18.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@solana/buffer-layout": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "license": "MIT", + "dependencies": { + "buffer": "~6.0.3" + }, + "engines": { + "node": ">=5.10" + } + }, + "node_modules/@solana/codecs-core": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz", + "integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/codecs-numbers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz", + "integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/errors": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz", + "integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js": { + "version": "1.98.4", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", + "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "@noble/curves": "^1.4.2", + "@noble/hashes": "^1.4.0", + "@solana/buffer-layout": "^4.0.1", + "@solana/codecs-numbers": "^2.1.0", + "agentkeepalive": "^4.5.0", + "bn.js": "^5.2.1", + "borsh": "^0.7.0", + "bs58": "^4.0.1", + "buffer": "6.0.3", + "fast-stable-stringify": "^1.0.0", + "jayson": "^4.1.1", + "node-fetch": "^2.7.0", + "rpc-websockets": "^9.0.2", + "superstruct": "^2.0.2" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/base-x": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz", + "integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==", + "license": "MIT" + }, + "node_modules/borsh": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + } + }, + "node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bufferutil": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", + "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "license": "MIT", + "dependencies": { + "es6-promise": "^4.0.3" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "engines": { + "node": "> 0.1.90" + } + }, + "node_modules/fast-stable-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "license": "MIT" + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jayson": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.3.0.tgz", + "integrity": "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==", + "license": "MIT", + "dependencies": { + "@types/connect": "^3.4.33", + "@types/node": "^12.12.54", + "@types/ws": "^7.4.4", + "commander": "^2.20.3", + "delay": "^5.0.0", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "isomorphic-ws": "^4.0.1", + "json-stringify-safe": "^5.0.1", + "stream-json": "^1.9.1", + "uuid": "^8.3.2", + "ws": "^7.5.10" + }, + "bin": { + "jayson": "bin/jayson.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jayson/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/rpc-websockets": { + "version": "9.3.9", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.9.tgz", + "integrity": "sha512-2iQDaTB4g5fDB2ihrTFSJSibCEuxaRi1q7qTW7ZO9/M5/TC+ToHA4D9/ffNLEbAoHNNrcdeP05oATNk44SKZXA==", + "license": "LGPL-3.0-only", + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/uuid": "^10.0.0", + "@types/ws": "^8.2.2", + "buffer": "^6.0.3", + "eventemitter3": "^5.0.1", + "uuid": "^14.0.0", + "ws": "^8.5.0" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + }, + "optionalDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^6.0.0" + } + }, + "node_modules/rpc-websockets/node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/rpc-websockets/node_modules/utf-8-validate": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz", + "integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/rpc-websockets/node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/rpc-websockets/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, + "node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "7.5.11", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", + "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/benchmark/sbf-composite/package.json b/benchmark/sbf-composite/package.json new file mode 100644 index 0000000..16c4437 --- /dev/null +++ b/benchmark/sbf-composite/package.json @@ -0,0 +1,15 @@ +{ + "name": "solmath-sbf-composite-benchmark", + "version": "0.0.0", + "private": true, + "type": "module", + "engines": { + "node": ">=20.18.0" + }, + "dependencies": { + "@solana/web3.js": "1.98.4" + }, + "overrides": { + "uuid": "11.1.1" + } +} diff --git a/benchmark/sbf-composite/src/lib.rs b/benchmark/sbf-composite/src/lib.rs new file mode 100644 index 0000000..457fcc4 --- /dev/null +++ b/benchmark/sbf-composite/src/lib.rs @@ -0,0 +1,475 @@ +use anchor_lang::prelude::*; +use solmath::*; + +declare_id!("BdR4cSgZGQgXNo33SZSYQXy7XgEK61sHT4NQaAkc3PBm"); + +const fn measurement_phi2_grid() -> [[i32; PHI2_GRID_SIZE]; PHI2_GRID_SIZE] { + let mut values = [[0; PHI2_GRID_SIZE]; PHI2_GRID_SIZE]; + let mut row = 0; + while row < PHI2_GRID_SIZE { + let mut column = 0; + while column < PHI2_GRID_SIZE { + values[row][column] = ((row * PHI2_GRID_SIZE + column) * 244) as i32; + column += 1; + } + row += 1; + } + values +} + +static MEASUREMENT_PHI2_TABLE: Phi2Table = Phi2Table::from_array(measurement_phi2_grid()); + +#[inline(always)] +fn log_cu() { + #[cfg(target_os = "solana")] + unsafe { + solana_msg::syscalls::sol_log_compute_units_(); + } +} + +macro_rules! measured { + ($expr:expr) => {{ + log_cu(); + let result = $expr; + log_cu(); + let succeeded = result.is_ok(); + let _ = core::hint::black_box(result); + msg!("succeeded = {}", succeeded); + Ok(()) + }}; +} + +#[program] +pub mod solmath_sbf_composite { + use super::*; + + pub fn compute_ln(_ctx: Context, x: u128) -> Result<()> { + measured!(ln_fixed_i(x)) + } + + pub fn compute_exp(_ctx: Context, x: i128) -> Result<()> { + measured!(exp_fixed_i(x)) + } + + pub fn compute_cdf(_ctx: Context, x: i128) -> Result<()> { + measured!(norm_cdf_poly(x)) + } + + pub fn compute_ln_hp(_ctx: Context, x: i128) -> Result<()> { + measured!(ln_fixed_hp(x)) + } + + pub fn compute_exp_hp(_ctx: Context, x: i128) -> Result<()> { + measured!(exp_fixed_hp(x)) + } + + pub fn compute_cdf_hp(_ctx: Context, x: i128) -> Result<()> { + measured!(norm_cdf_poly_hp(x)) + } + + pub fn compute_div_hp(_ctx: Context, a: i128, b: i128) -> Result<()> { + measured!(fp_div_hp_safe(a, b)) + } + + pub fn compute_pdf(_ctx: Context, x: i128) -> Result<()> { + measured!(norm_pdf(x)) + } + + pub fn compute_pow(_ctx: Context, base: u128, exponent: u128) -> Result<()> { + measured!(pow_fixed(base, exponent)) + } + + pub fn compute_pow_i(_ctx: Context, base: i128, exponent: i128) -> Result<()> { + measured!(pow_fixed_i(base, exponent)) + } + + pub fn compute_pow_int(_ctx: Context, base: u128, exponent: u128) -> Result<()> { + measured!(pow_int(base, exponent)) + } + + pub fn compute_inverse_cdf(_ctx: Context, probability: i128) -> Result<()> { + measured!(inverse_norm_cdf(probability)) + } + + pub fn compute_cdf_pdf(_ctx: Context, x: i128) -> Result<()> { + measured!(norm_cdf_and_pdf(x)) + } + + pub fn compute_cdf_pdf_poly(_ctx: Context, x: i128) -> Result<()> { + measured!(norm_cdf_and_pdf_poly(x)) + } + + pub fn compute_mul_i_round(_ctx: Context, a: i128, b: i128) -> Result<()> { + measured!(fp_mul_i_round(a, b)) + } + + pub fn compute_mul_i_fast(_ctx: Context, a: i128, b: i128) -> Result<()> { + measured!(Ok::(a.wrapping_mul(b) / SCALE_I)) + } + + pub fn compute_mul_i_fast_round(_ctx: Context, a: i128, b: i128) -> Result<()> { + measured!({ + let product = a.wrapping_mul(b); + let half = SCALE_I / 2; + Ok::(if product >= 0 { + product.wrapping_add(half) / SCALE_I + } else { + -product.wrapping_neg().wrapping_add(half) / SCALE_I + }) + }) + } + + pub fn compute_div_i(_ctx: Context, a: i128, b: i128) -> Result<()> { + measured!(fp_div_i(a, b)) + } + + pub fn compute_bs_price( + _ctx: Context, + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, + ) -> Result<()> { + measured!(black_scholes_price(s, k, r, sigma, t)) + } + + pub fn compute_bs_full( + _ctx: Context, + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, + ) -> Result<()> { + measured!(bs_full(s, k, r, sigma, t)) + } + + pub fn compute_bs_delta( + _ctx: Context, + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, + ) -> Result<()> { + measured!(bs_delta(s, k, r, sigma, t)) + } + + pub fn compute_bs_gamma( + _ctx: Context, + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, + ) -> Result<()> { + measured!(bs_gamma(s, k, r, sigma, t)) + } + + pub fn compute_bs_vega( + _ctx: Context, + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, + ) -> Result<()> { + measured!(bs_vega(s, k, r, sigma, t)) + } + + pub fn compute_bs_theta( + _ctx: Context, + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, + ) -> Result<()> { + measured!(bs_theta(s, k, r, sigma, t)) + } + + pub fn compute_bs_rho( + _ctx: Context, + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, + ) -> Result<()> { + measured!(bs_rho(s, k, r, sigma, t)) + } + + pub fn compute_iv( + _ctx: Context, + market_price: u128, + s: u128, + k: u128, + r: u128, + t: u128, + ) -> Result<()> { + measured!(implied_vol(market_price, s, k, r, t)) + } + + #[allow(clippy::too_many_arguments)] + pub fn compute_asian( + _ctx: Context, + s: u128, + k: u128, + r: u128, + q: u128, + sigma: u128, + t: u128, + averaging_time: u128, + fixed_average: u128, + fixed_weight: u128, + ) -> Result<()> { + measured!(arithmetic_asian_price( + s, + k, + r, + q, + sigma, + t, + averaging_time, + fixed_average, + fixed_weight, + )) + } + + #[allow(clippy::too_many_arguments)] + pub fn compute_sabr_vol( + _ctx: Context, + f: u128, + k: u128, + t: u128, + alpha: u128, + beta: u128, + rho: i128, + nu: u128, + ) -> Result<()> { + measured!(sabr_implied_vol(f, k, t, alpha, beta, rho, nu)) + } + + #[allow(clippy::too_many_arguments)] + pub fn compute_sabr_price( + _ctx: Context, + s: u128, + k: u128, + r: u128, + t: u128, + alpha: u128, + beta: u128, + rho: i128, + nu: u128, + ) -> Result<()> { + measured!(sabr_price(s, k, r, t, alpha, beta, rho, nu)) + } + + #[allow(clippy::too_many_arguments)] + pub fn compute_sabr_greeks( + _ctx: Context, + s: u128, + k: u128, + r: u128, + t: u128, + alpha: u128, + beta: u128, + rho: i128, + nu: u128, + ) -> Result<()> { + measured!(sabr_greeks(s, k, r, t, alpha, beta, rho, nu)) + } + + #[allow(clippy::too_many_arguments)] + pub fn compute_sabr_precomputed_vol( + _ctx: Context, + f: u128, + k: u128, + t: u128, + alpha: u128, + beta: u128, + rho: i128, + nu: u128, + ) -> Result<()> { + measured!(sabr_precompute(f, t, alpha, beta, rho, nu) + .and_then(|precomputed| sabr_vol_at(&precomputed, k))) + } + + pub fn compute_sabr_precompute( + _ctx: Context, + f: u128, + t: u128, + alpha: u128, + beta: u128, + rho: i128, + nu: u128, + ) -> Result<()> { + measured!(sabr_precompute(f, t, alpha, beta, rho, nu)) + } + + #[allow(clippy::too_many_arguments)] + pub fn compute_sabr_vol_at( + _ctx: Context, + f: u128, + k: u128, + t: u128, + alpha: u128, + beta: u128, + rho: i128, + nu: u128, + ) -> Result<()> { + let precomputed = sabr_precompute(f, t, alpha, beta, rho, nu); + match precomputed { + Ok(precomputed) => measured!(sabr_vol_at(&precomputed, k)), + Err(error) => measured!(Err::(error)), + } + } + + pub fn compute_sabr_z_over_chi(_ctx: Context, z: i128, rho: i128) -> Result<()> { + measured!(sabr_z_over_chi_pade(z, rho)) + } + + pub fn compute_bvn(_ctx: Context, a: i128, b: i128, rho: i128) -> Result<()> { + measured!(bvn_cdf(a, b, rho)) + } + + pub fn compute_bvn_hp(_ctx: Context, a: i128, b: i128, rho: i128) -> Result<()> { + measured!(bvn_cdf_hp(a, b, rho)) + } + + #[allow(clippy::too_many_arguments)] + pub fn compute_heston( + _ctx: Context, + s: u128, + k: u128, + r: u128, + t: u128, + v0: u128, + kappa: u128, + theta: u128, + xi: u128, + rho: i128, + ) -> Result<()> { + measured!(heston_price(s, k, r, t, v0, kappa, theta, xi, rho)) + } + + #[allow(clippy::too_many_arguments)] + pub fn compute_nig( + _ctx: Context, + s: u128, + k: u128, + r: u128, + t: u128, + alpha: u128, + beta: i128, + delta: u128, + ) -> Result<()> { + measured!(nig_call_price(s, k, r, t, alpha, beta, delta)) + } + + #[allow(clippy::too_many_arguments)] + pub fn compute_nig_certified( + _ctx: Context, + s: u128, + k: u128, + r: i128, + q: i128, + t: u128, + alpha: u128, + beta: i128, + delta: u128, + requested_max_abs_error: u128, + ) -> Result<()> { + measured!(nig_price_certified( + s, + k, + r, + q, + t, + NigParams { + alpha, + beta, + delta_per_year: delta, + }, + requested_max_abs_error, + )) + } + + #[allow(clippy::too_many_arguments)] + pub fn compute_nig_64( + _ctx: Context, + s: i64, + k: i64, + r: i64, + t: i64, + alpha: i64, + beta: i64, + delta: i64, + ) -> Result<()> { + measured!(nig_call_64(s, k, r, t, alpha, beta, delta)) + } + + #[allow(clippy::too_many_arguments)] + pub fn compute_nig_put_64( + _ctx: Context, + s: i64, + k: i64, + r: i64, + t: i64, + alpha: i64, + beta: i64, + delta: i64, + ) -> Result<()> { + measured!(nig_put_64(s, k, r, t, alpha, beta, delta)) + } + + pub fn compute_phi2_eval(_ctx: Context, a: i128, b: i128) -> Result<()> { + let table = core::hint::black_box(&MEASUREMENT_PHI2_TABLE); + measured!(table.eval(a, b)) + } + + pub fn compute_american_kbi_call( + _ctx: Context, + spot: u128, + strike: u128, + rate: u128, + dividend_yield: u128, + sigma: u128, + maturity: u128, + ) -> Result<()> { + measured!(american_kbi_price( + spot, + strike, + rate, + dividend_yield, + sigma, + maturity, + AmericanKbiKind::Call, + )) + } + + pub fn compute_american_kbi_put( + _ctx: Context, + spot: u128, + strike: u128, + rate: u128, + dividend_yield: u128, + sigma: u128, + maturity: u128, + ) -> Result<()> { + measured!(american_kbi_price( + spot, + strike, + rate, + dividend_yield, + sigma, + maturity, + AmericanKbiKind::Put, + )) + } + +} + +#[derive(Accounts)] +pub struct Measure {} diff --git a/benchmark/sbf-footprint/Cargo.lock b/benchmark/sbf-footprint/Cargo.lock new file mode 100644 index 0000000..1f2e1cf --- /dev/null +++ b/benchmark/sbf-footprint/Cargo.lock @@ -0,0 +1,1631 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anchor-attribute-access-control" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a883ca44ef14b2113615fc6d3a85fefc68b5002034e88db37f7f1f802f88aa9" +dependencies = [ + "anchor-syn", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-account" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c4d97763b29030412b4b80715076377edc9cc63bc3c9e667297778384b9fd2" +dependencies = [ + "anchor-syn", + "bs58", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-constant" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae3328bbf9bbd517a51621b1ba6cbec06cbbc25e8cfc7403bddf69bcf088206" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-error" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2398a6d9e16df1ee9d7d37d970a8246756de898c8dd16ef6bdbe4da20cf39a" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-event" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12758f4ec2f0e98d4d56916c6fe95cb23d74b8723dd902c762c5ef46ebe7b65" +dependencies = [ + "anchor-syn", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-attribute-program" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7193b5af2649813584aae6e3569c46fd59616a96af2083c556b13136c3830f" +dependencies = [ + "anchor-lang-idl", + "anchor-syn", + "anyhow", + "bs58", + "heck", + "proc-macro2", + "quote", + "serde_json", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-accounts" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d332d1a13c0fca1a446de140b656e66110a5e8406977dcb6a41e5d6f323760b0" +dependencies = [ + "anchor-syn", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-serde" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8656e4af182edaeae665fa2d2d7ee81148518b5bd0be9a67f2a381bb17da7d46" +dependencies = [ + "anchor-syn", + "borsh-derive-internal", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-derive-space" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcff2a083560cd79817db07d89a4de39a2c4b2eaa00c1742cf0df49b25ff2bed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "anchor-lang" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67d85d5376578f12d840c29ff323190f6eecd65b00a0b5f2b2f232751d049cc" +dependencies = [ + "anchor-attribute-access-control", + "anchor-attribute-account", + "anchor-attribute-constant", + "anchor-attribute-error", + "anchor-attribute-event", + "anchor-attribute-program", + "anchor-derive-accounts", + "anchor-derive-serde", + "anchor-derive-space", + "anchor-lang-idl", + "base64 0.21.7", + "bincode", + "borsh 0.10.4", + "bytemuck", + "solana-account-info", + "solana-clock", + "solana-cpi", + "solana-define-syscall", + "solana-feature-gate-interface", + "solana-instruction", + "solana-instructions-sysvar", + "solana-invoke", + "solana-loader-v3-interface", + "solana-msg", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", + "solana-sysvar", + "solana-sysvar-id", + "thiserror", +] + +[[package]] +name = "anchor-lang-idl" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47914b4290ae2bdf4ec203aa821e6eba86d7c78ef497918938038dcc6919f953" +dependencies = [ + "anchor-lang-idl-spec", + "anyhow", + "heck", + "regex", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "anchor-lang-idl-spec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bdf143115440fe621bdac3a29a1f7472e09f6cd82b2aa569429a0c13f103838" +dependencies = [ + "anyhow", + "serde", +] + +[[package]] +name = "anchor-syn" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93b69aa7d099b59378433f6d7e20e1008fc10c69e48b220270e5b3f2ec4c8be" +dependencies = [ + "anyhow", + "bs58", + "cargo_toml", + "heck", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "syn 1.0.109", + "thiserror", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115e54d64eb62cdebad391c19efc9dce4981c690c85a33a12199d99bb9546fee" +dependencies = [ + "borsh-derive 0.10.4", + "hashbrown 0.13.2", +] + +[[package]] +name = "borsh" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" +dependencies = [ + "borsh-derive 1.7.0", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831213f80d9423998dd696e2c5345aba6be7a0bd8cd19e31c5243e13df1cef89" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" +dependencies = [ + "once_cell", + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65d6ba50644c98714aa2a70d13d7df3cd75cd2b523a2b452bf010443800976b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276691d96f063427be83e6692b86148e488ebba9f48f77788724ca027ba3b6d4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] + +[[package]] +name = "bytemuck" +version = "1.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cargo_toml" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a98356df42a2eb1bd8f1793ae4ee4de48e384dd974ce5eac8eee802edb7492be" +dependencies = [ + "serde", + "toml 0.8.23", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rand_core", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "five8" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75b8549488b4715defcb0d8a8a1c1c76a80661b5fa106b4ca0e7fce59d7d875" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_const" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26dec3da8bc3ef08f2c04f61eab298c3ab334523e55f076354d6d6f613799a7b" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2551bf44bc5f776c15044b9b94153a00198be06743e262afaaa61f11ac7523a5" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml 0.5.11", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.5+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "solana-account-info" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8f5152a288ef1912300fc6efa6c2d1f9bb55d9398eb6c72326360b8063987da" +dependencies = [ + "solana-program-error", + "solana-program-memory", + "solana-pubkey", +] + +[[package]] +name = "solana-atomic-u64" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52e52720efe60465b052b9e7445a01c17550666beec855cce66f44766697bc2" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "solana-clock" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8584296123df8fe229b95e2ebfd37ae637fe9db9b7d4dd677ac5a78e80dbfce" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-cpi" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc71126edddc2ba014622fc32d0f5e2e78ec6c5a1e0eb511b85618c09e9ea11" +dependencies = [ + "solana-account-info", + "solana-define-syscall", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-stable-layout", +] + +[[package]] +name = "solana-decode-error" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c781686a18db2f942e70913f7ca15dc120ec38dcab42ff7557db2c70c625a35" +dependencies = [ + "num-traits", +] + +[[package]] +name = "solana-define-syscall" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae3e2abcf541c8122eafe9a625d4d194b4023c20adde1e251f94e056bb1aee2" + +[[package]] +name = "solana-epoch-rewards" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b575d3dd323b9ea10bb6fe89bf6bf93e249b215ba8ed7f68f1a3633f384db7" +dependencies = [ + "serde", + "serde_derive", + "solana-hash", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-epoch-schedule" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fce071fbddecc55d727b1d7ed16a629afe4f6e4c217bc8d00af3b785f6f67ed" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-feature-gate-interface" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f5c5382b449e8e4e3016fb05e418c53d57782d8b5c30aa372fc265654b956d" +dependencies = [ + "solana-pubkey", + "solana-sdk-ids", +] + +[[package]] +name = "solana-fee-calculator" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89bc408da0fb3812bc3008189d148b4d3e08252c79ad810b245482a3f70cd8d" +dependencies = [ + "log", + "serde", + "serde_derive", +] + +[[package]] +name = "solana-hash" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b96e9f0300fa287b545613f007dfe20043d7812bee255f418c1eb649c93b63" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "five8", + "js-sys", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-sanitize", + "wasm-bindgen", +] + +[[package]] +name = "solana-instruction" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab5682934bd1f65f8d2c16f21cb532526fcc1a09f796e2cacdb091eee5774ad" +dependencies = [ + "bincode", + "getrandom", + "js-sys", + "num-traits", + "serde", + "serde_derive", + "serde_json", + "solana-define-syscall", + "solana-pubkey", + "wasm-bindgen", +] + +[[package]] +name = "solana-instructions-sysvar" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0e85a6fad5c2d0c4f5b91d34b8ca47118fc593af706e523cdbedf846a954f57" +dependencies = [ + "bitflags", + "solana-account-info", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-sanitize", + "solana-sdk-ids", + "solana-serialize-utils", + "solana-sysvar-id", +] + +[[package]] +name = "solana-invoke" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f5693c6de226b3626658377168b0184e94e8292ff16e3d31d4766e65627565" +dependencies = [ + "solana-account-info", + "solana-define-syscall", + "solana-instruction", + "solana-program-entrypoint", + "solana-stable-layout", +] + +[[package]] +name = "solana-last-restart-slot" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6360ac2fdc72e7463565cd256eedcf10d7ef0c28a1249d261ec168c1b55cdd" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-loader-v3-interface" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4be76cfa9afd84ca2f35ebc09f0da0f0092935ccdac0595d98447f259538c2" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", +] + +[[package]] +name = "solana-msg" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36a1a14399afaabc2781a1db09cb14ee4cc4ee5c7a5a3cfcc601811379a8092" +dependencies = [ + "solana-define-syscall", +] + +[[package]] +name = "solana-program-entrypoint" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ce041b1a0ed275290a5008ee1a4a6c48f5054c8a3d78d313c08958a06aedbd" +dependencies = [ + "solana-account-info", + "solana-msg", + "solana-program-error", + "solana-pubkey", +] + +[[package]] +name = "solana-program-error" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee2e0217d642e2ea4bee237f37bd61bb02aec60da3647c48ff88f6556ade775" +dependencies = [ + "borsh 1.7.0", + "num-traits", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-pubkey", +] + +[[package]] +name = "solana-program-memory" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a5426090c6f3fd6cfdc10685322fede9ca8e5af43cd6a59e98bfe4e91671712" +dependencies = [ + "solana-define-syscall", +] + +[[package]] +name = "solana-program-option" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc677a2e9bc616eda6dbdab834d463372b92848b2bfe4a1ed4e4b4adba3397d0" + +[[package]] +name = "solana-program-pack" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "319f0ef15e6e12dc37c597faccb7d62525a509fec5f6975ecb9419efddeb277b" +dependencies = [ + "solana-program-error", +] + +[[package]] +name = "solana-pubkey" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b62adb9c3261a052ca1f999398c388f1daf558a1b492f60a6d9e64857db4ff1" +dependencies = [ + "borsh 0.10.4", + "borsh 1.7.0", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "five8", + "five8_const", + "getrandom", + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-decode-error", + "solana-define-syscall", + "solana-sanitize", + "solana-sha256-hasher", + "wasm-bindgen", +] + +[[package]] +name = "solana-rent" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1aea8fdea9de98ca6e8c2da5827707fb3842833521b528a713810ca685d2480" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sanitize" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f1bc1357b8188d9c4a3af3fc55276e56987265eb7ad073ae6f8180ee54cecf" + +[[package]] +name = "solana-sdk-ids" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5d8b9cc68d5c88b062a33e23a6466722467dde0035152d8fb1afbcdf350a5f" +dependencies = [ + "solana-pubkey", +] + +[[package]] +name = "solana-sdk-macro" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86280da8b99d03560f6ab5aca9de2e38805681df34e0bb8f238e69b29433b9df" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "solana-serialize-utils" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "817a284b63197d2b27afdba829c5ab34231da4a9b4e763466a003c40ca4f535e" +dependencies = [ + "solana-instruction", + "solana-pubkey", + "solana-sanitize", +] + +[[package]] +name = "solana-sha256-hasher" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa3feb32c28765f6aa1ce8f3feac30936f16c5c3f7eb73d63a5b8f6f8ecdc44" +dependencies = [ + "sha2", + "solana-define-syscall", + "solana-hash", +] + +[[package]] +name = "solana-slot-hashes" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8691982114513763e88d04094c9caa0376b867a29577939011331134c301ce" +dependencies = [ + "serde", + "serde_derive", + "solana-hash", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-slot-history" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ccc1b2067ca22754d5283afb2b0126d61eae734fc616d23871b0943b0d935e" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-sdk-ids", + "solana-sysvar-id", +] + +[[package]] +name = "solana-stable-layout" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f14f7d02af8f2bc1b5efeeae71bc1c2b7f0f65cd75bcc7d8180f2c762a57f54" +dependencies = [ + "solana-instruction", + "solana-pubkey", +] + +[[package]] +name = "solana-stake-interface" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5269e89fde216b4d7e1d1739cf5303f8398a1ff372a81232abbee80e554a838c" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-clock", + "solana-cpi", + "solana-decode-error", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-system-interface", + "solana-sysvar-id", +] + +[[package]] +name = "solana-system-interface" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d7c18cb1a91c6be5f5a8ac9276a1d7c737e39a21beba9ea710ab4b9c63bc90" +dependencies = [ + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-decode-error", + "solana-instruction", + "solana-pubkey", + "wasm-bindgen", +] + +[[package]] +name = "solana-sysvar" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c3595f95069f3d90f275bb9bd235a1973c4d059028b0a7f81baca2703815db" +dependencies = [ + "base64 0.22.1", + "bincode", + "lazy_static", + "serde", + "serde_derive", + "solana-account-info", + "solana-clock", + "solana-define-syscall", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-hash", + "solana-instruction", + "solana-instructions-sysvar", + "solana-last-restart-slot", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-pubkey", + "solana-rent", + "solana-sanitize", + "solana-sdk-ids", + "solana-sdk-macro", + "solana-slot-hashes", + "solana-slot-history", + "solana-stake-interface", + "solana-sysvar-id", +] + +[[package]] +name = "solana-sysvar-id" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5762b273d3325b047cfda250787f8d796d781746860d5d0a746ee29f3e8812c1" +dependencies = [ + "solana-pubkey", + "solana-sdk-ids", +] + +[[package]] +name = "solmath" +version = "0.2.0" + +[[package]] +name = "solmath-sbf-footprint" +version = "0.0.0" +dependencies = [ + "anchor-lang", + "indexmap", + "solmath", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.0.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" +dependencies = [ + "indexmap", + "toml_datetime 1.0.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.0.10+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "zerocopy" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/benchmark/sbf-footprint/Cargo.toml b/benchmark/sbf-footprint/Cargo.toml new file mode 100644 index 0000000..f5a0411 --- /dev/null +++ b/benchmark/sbf-footprint/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "solmath-sbf-footprint" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib", "lib"] +name = "solmath_sbf_footprint" + +[features] +default = [] +exp = [] +expm1 = [] +ln1p = [] +both = [] +legacy-expm1 = [] +kbi = ["solmath/american-kbi"] +nig = ["solmath/nig"] +cpi = ["no-entrypoint"] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +custom-heap = [] +custom-panic = [] +anchor-debug = [] +idl-build = ["anchor-lang/idl-build"] + +[dependencies] +anchor-lang = "=0.32.1" +indexmap = "=2.13.0" +solmath = { path = "../..", default-features = false, features = ["transcendental"] } + +[profile.release] +overflow-checks = true +lto = "fat" +codegen-units = 1 diff --git a/benchmark/sbf-footprint/src/lib.rs b/benchmark/sbf-footprint/src/lib.rs new file mode 100644 index 0000000..f84ef76 --- /dev/null +++ b/benchmark/sbf-footprint/src/lib.rs @@ -0,0 +1,146 @@ +use anchor_lang::prelude::*; + +#[cfg(feature = "legacy-expm1")] +use solmath::{exp_fixed_i, fp_mul_i_round, SolMathError, SCALE_I}; + +#[cfg(any( + all(feature = "exp", feature = "expm1"), + all(feature = "exp", feature = "ln1p"), + all(feature = "exp", feature = "both"), + all(feature = "exp", feature = "legacy-expm1"), + all(feature = "expm1", feature = "ln1p"), + all(feature = "expm1", feature = "both"), + all(feature = "expm1", feature = "legacy-expm1"), + all(feature = "ln1p", feature = "both"), + all(feature = "ln1p", feature = "legacy-expm1"), + all(feature = "both", feature = "legacy-expm1"), +))] +compile_error!("select at most one footprint variant"); + +#[cfg(all( + feature = "kbi", + any( + feature = "exp", + feature = "expm1", + feature = "ln1p", + feature = "both", + feature = "legacy-expm1" + ) +))] +compile_error!("select at most one footprint variant"); + +#[cfg(all( + feature = "nig", + any( + feature = "exp", + feature = "expm1", + feature = "ln1p", + feature = "both", + feature = "legacy-expm1", + feature = "kbi" + ) +))] +compile_error!("select at most one footprint variant"); + +declare_id!("Dr5QfdFEChkNpR9bPcAdRXNLuc1gTu955EnhS5bBg8m5"); + +#[cfg(feature = "legacy-expm1")] +#[inline(never)] +fn legacy_expm1_fixed(x: i128) -> core::result::Result { + let half = SCALE_I / 2; + if x > half || x < -half { + return Ok(exp_fixed_i(x)? - SCALE_I); + } + if x == 0 { + return Ok(0); + } + + const C11: i128 = 25_052; + const C10: i128 = 275_573; + const C9: i128 = 2_755_732; + const C8: i128 = 24_801_587; + const C7: i128 = 198_412_698; + const C6: i128 = 1_388_888_889; + const C5: i128 = 8_333_333_333; + const C4: i128 = 41_666_666_667; + const C3: i128 = 166_666_666_667; + const C2: i128 = 500_000_000_000; + + let p = fp_mul_i_round(x, C11)? + C10; + let p = fp_mul_i_round(x, p)? + C9; + let p = fp_mul_i_round(x, p)? + C8; + let p = fp_mul_i_round(x, p)? + C7; + let p = fp_mul_i_round(x, p)? + C6; + let p = fp_mul_i_round(x, p)? + C5; + let p = fp_mul_i_round(x, p)? + C4; + let p = fp_mul_i_round(x, p)? + C3; + let p = fp_mul_i_round(x, p)? + C2; + let p = fp_mul_i_round(x, p)? + SCALE_I; + fp_mul_i_round(x, p) +} + +#[program] +pub mod solmath_sbf_footprint { + use super::*; + + #[allow(unused_variables)] + pub fn measure(_ctx: Context, x: i128) -> Result<()> { + #[cfg(feature = "exp")] + let result = solmath::exp_fixed_i(x).map_err(|_| FootprintError::MathError)?; + #[cfg(feature = "expm1")] + let result = solmath::expm1_fixed(x).map_err(|_| FootprintError::MathError)?; + #[cfg(feature = "ln1p")] + let result = solmath::ln_1p_fixed(x).map_err(|_| FootprintError::MathError)?; + #[cfg(feature = "both")] + let result = { + let expm1 = solmath::expm1_fixed(x).map_err(|_| FootprintError::MathError)?; + let ln1p = solmath::ln_1p_fixed(x).map_err(|_| FootprintError::MathError)?; + expm1.wrapping_add(ln1p) + }; + #[cfg(feature = "legacy-expm1")] + let result = legacy_expm1_fixed(x).map_err(|_| FootprintError::MathError)?; + #[cfg(feature = "kbi")] + let result = solmath::american_kbi_price( + 100 * solmath::SCALE, + 100 * solmath::SCALE, + 50_000_000_000, + 30_000_000_000, + 300_000_000_000, + solmath::SCALE, + solmath::AmericanKbiKind::Put, + ) + .map_err(|_| FootprintError::MathError)? as i128; + #[cfg(feature = "nig")] + let result = solmath::nig_call_price( + 100 * solmath::SCALE, + 100 * solmath::SCALE, + 50_000_000_000, + solmath::SCALE, + 10 * solmath::SCALE, + -2 * solmath::SCALE_I, + solmath::SCALE / 5, + ) + .map_err(|_| FootprintError::MathError)? as i128; + #[cfg(not(any( + feature = "exp", + feature = "expm1", + feature = "ln1p", + feature = "both", + feature = "legacy-expm1", + feature = "kbi", + feature = "nig" + )))] + let result = x; + + msg!("result = {}", result); + Ok(()) + } +} + +#[derive(Accounts)] +pub struct Measure {} + +#[error_code] +pub enum FootprintError { + MathError, +} diff --git a/docs/AMERICAN_KBI.md b/docs/AMERICAN_KBI.md new file mode 100644 index 0000000..f35a950 --- /dev/null +++ b/docs/AMERICAN_KBI.md @@ -0,0 +1,127 @@ +# Kim Boundary Integration (KBI) + +`american-kbi` is SolMath's fully on-chain American-option engine. It accepts +only `(S, K, r, q, sigma, T)` plus the call/put selector and performs every +parameter-dependent calculation in the program. It does not consume a price +surface, uploaded operator, account, matrix, oracle-built coefficient set, or +trusted off-chain result. + +## Method + +KBI combines Kim's early-exercise-premium representation with a directly +reconstructed smooth-pasting exercise boundary: + +1. Normalize the put problem by strike. Calls use exact American put-call + duality, swapping `(S, K, r, q)`. +2. Reconstruct the nonlinear put exercise boundary on an 18-node graded time + grid. +3. Substitute `lag = t*y^2`, cancelling the square-root singularity exactly, + and evaluate both boundary-history integrals at six fixed Gaussian nodes. + All 18 positive boundary values remain available for log interpolation. +4. Integrate the early-exercise premium at nine globally transformed nodes. + Their positive weights are QdFp-regularized empirical cubature weights fit + once on the 48-contract training corpus. + +The embedded artifact contains only parameter-independent grid geometry, nine +global cubature weights, and cubic-Hermite normal-kernel coefficients. It +contains no sampled option prices or per-contract coefficients. The live +boundary, discount factors, normal kernels, exercise decision, and premium are +computed from the six quote inputs on-chain. + +## API and runtime domain + +All numeric inputs and the returned price use SolMath's `1e12` scale. + +```rust +use solmath::{american_kbi_price, AmericanKbiKind, SCALE}; + +let price = american_kbi_price( + 100 * SCALE, // spot + 100 * SCALE, // strike + 50_000_000_000, // r = 5% + 30_000_000_000, // q = 3% + 300_000_000_000, // sigma = 30% + SCALE, // T = 1 year + AmericanKbiKind::Put, +).expect("inputs are inside the KBI domain"); +``` + +The runtime contract covers the following parameter box; inputs outside it +return `DomainError`: + +- `0 <= r,q <= 12%` +- `10% <= sigma <= 120%` +- `30/365 <= T <= 2` years +- `|ln(S/K)| <= 0.75` + +Enable it independently with: + +```toml +solmath = { version = "0.2", default-features = false, features = ["american-kbi"] } +``` + +## Accuracy against QuantLib QdFp + +Errors below are dollars at a `$100` strike. The reference is QuantLib 1.41 +`QdFpAmericanEngine` with `accurateScheme`. + +| Corpus | Leg | Comparisons | Median | P95 | P99 | Maximum | +|---|---:|---:|---:|---:|---:|---:| +| Held-out | Call | 792 | $0.000089 | $0.001077 | $0.001727 | $0.001885 | +| Held-out | Put | 792 | $0.000094 | $0.001618 | $0.002030 | $0.002095 | +| Unseen deterministic | Call | 6,336 | $0.000113 | $0.001404 | $0.002059 | $0.002502 | +| Unseen deterministic | Put | 6,336 | $0.000097 | $0.001305 | $0.001919 | $0.002698 | + +The unseen corpus contains 192 newly sampled contracts and 33 log-moneyness +points per contract per leg. Reports: + +- [`american_kbi_runtime_accuracy_report.json`](https://github.com/DJBarker87/solmath/blob/v0.2.0/benchmark/american_kbi_runtime_accuracy_report.json) +- [`american_kbi_unseen_accuracy_report.json`](https://github.com/DJBarker87/solmath/blob/v0.2.0/benchmark/american_kbi_unseen_accuracy_report.json) + +The release comparison also executed every row of the existing 100,000-vector +production corpus and every row of the 10,000-vector adversarial corpus. There +was no sampling. The table reports KBI error on every input inside its declared +domain; all such inputs were accepted. + +| Corpus | Leg | Requested | In domain / accepted | Median | P95 | P99 | Maximum | Within $0.001 | +|---|---:|---:|---:|---:|---:|---:|---:|---:| +| Production 100K | Call | 100,000 | 78,047 | $0.0000156 | $0.0006946 | $0.0013619 | $0.0027441 | 97.57% | +| Production 100K | Put | 100,000 | 78,047 | $0.0000160 | $0.0007078 | $0.0014208 | $0.0032638 | 97.46% | +| Adversarial 10K | Call | 10,000 | 5,528 | $0.0000234 | $0.0011672 | $0.0017998 | $0.0029456 | 92.37% | +| Adversarial 10K | Put | 10,000 | 5,528 | $0.0000279 | $0.0013621 | $0.0019915 | $0.0027250 | 91.08% | + +Every accepted KBI quote was within `$0.01` per `$100` strike. In the +production corpus, the 21,953 excluded inputs per leg consist exactly of 5,094 +maturities below 30 days and 16,859 above two years. In the adversarial corpus, +5,528 inputs per leg are in-domain and 4,472 intentionally target at least one +declared domain boundary. No supported input was rejected. + +## Deployed compute and footprint + +The retained 2,000-quote-per-leg Agave campaign exercised KBI with generated +inputs. Every quote was accepted. + +| Leg | Accepted | Average math CU | Median | P95 | P99 | Max math CU | Max full instruction CU | +|---|---:|---:|---:|---:|---:|---:|---:| +| Call | 2,000 / 2,000 | 381,096 | 382,636 | 387,937 | 388,749 | 389,587 | 390,628 | +| Put | 2,000 / 2,000 | 371,876 | 382,652 | 387,882 | 388,879 | 389,742 | 390,786 | + +The isolated KBI deployment measured 293,360 bytes against a 184,848-byte +Anchor baseline: a 108,512-byte linked delta. The measured full composite was +1,083,408 bytes with SHA-256 +`685d49886179ca0bec80e31d9e9b878f3d044a47869cfd2f70cc2cf194c05161`. See +[`american_kbi_release_report.json`](https://github.com/DJBarker87/solmath/blob/v0.2.0/benchmark/american_kbi_release_report.json). + +## Reproducibility + +The embedded Q40 artifact is identified by SHA-256 +`6c0e7857669b9913770de45da32d5cfdeb1d89faf78e7bcdad55d3ee27a218ca`. +`scripts/generate_american_kbi_data.py` regenerates the parameter-independent +payload, `scripts/fit_american_kbi_price_weights.py --check` reproduces the +nine positive cubature weights from the fixed 48-contract training corpus, and +`scripts/validate_american_kbi_runtime.py` drives the compiled Rust batch +executable against QuantLib. These are release/validation tools only; none +participates in a live quote. + +Together, the generator, artifact identity, QdFp corpora, and deployed-CU run +bind the documented algorithm to the measured `0.2.0` implementation. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f5c3da2..872b918 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -36,8 +36,10 @@ integers in instruction data. ### Signedness -- **Prices, rates, volatilities, times** — `u128` (always non-negative). -- **Intermediate results, Greeks, correlation** — `i128` (signed). +- **Prices, volatilities, times, and conventional model rates** — generally + `u128`. +- **Intermediate results, Greeks, correlation, and NIG rates/yields** — `i128` + where the quantity can be signed. - The convention is enforced by function signatures: if a parameter is `u128`, the caller cannot pass a negative value. @@ -50,18 +52,19 @@ The library operates at two scales: | Tier | Constant | Scale | Precision | Used by | |------|----------|-------|-----------|---------| | **Standard** | `SCALE` / `SCALE_I` | 1e12 | 12 decimal places | Core arithmetic, transcendentals, trig, standard BS | -| **High-Precision (HP)** | `SCALE_HP` / `SCALE_HP_U` | 1e15 | 15 decimal places | HP ln/exp, HP BS, barrier, HP CDF | +| **High-Precision (HP)** | `SCALE_HP` / `SCALE_HP_U` | 1e15 | 15 decimal places | HP ln/exp, HP BS, barrier, Asian/TWAP moments, HP CDF | ### Why two tiers? -Standard precision (1e12) is optimal for Solana: +Standard precision (1e12) is the default Solana tier: - `u128` can hold values up to ~3.4e38, so two SCALE-valued numbers can always be multiplied without overflow: `1e12 * 1e12 = 1e24 << 3.4e38`. -- 12 decimal places exceed the precision of any real-world financial parameter. +- 12 decimal places provide fine-grained quote, rate, and probability inputs. - CU costs are low: simple multiplies are ~50 CU. HP (1e15) is needed when errors compound through chains of operations. A single -`ln` at 1e12 has max 3 ULP error. Pass that through `exp`, `norm_cdf`, and two +`ln` at 1e12 has a proved 3-ULP all-input bound and measured max 2 ULP error. +Pass that through `exp`, `norm_cdf`, and two more multiplies (as Black-Scholes does), and errors can amplify to 100+ ULP at standard precision. By computing the chain at 1e15 and rounding back to 1e12 at the end, the library preserves 10-14 significant figures. @@ -139,24 +142,31 @@ solmath/src/ ├── mul_div.rs mul_div_floor/ceil at u64 and u128 ├── double_word.rs DoubleWord sub-ULP accumulator ├── encoding.rs fp/fp_i decimal parsing helpers +├── checked.rs Validated financial input types │ ├── transcendental.rs ln_fixed_i, exp_fixed_i, pow_fixed, expm1_fixed +├── exp_coeffs.rs generated Q22 minimax / fractional-power constants ├── trig.rs sin_fixed, cos_fixed, sincos_fixed ├── normal.rs norm_pdf, norm_cdf_poly, inverse_norm_cdf +├── norm_cdf_coeffs.rs generated guarded CDF coefficients ├── hp.rs HP variants: ln/exp/pow/mul/div/BS at 1e15 │ ├── complex.rs Complex type, mul/div/exp/sqrt ├── bs.rs Standard-precision Black-Scholes ├── iv.rs Implied volatility solver ├── barrier.rs European barrier options (Rubinstein-Reiner) -├── heston.rs Heston stochastic vol (three-path architecture) +├── asian.rs Continuous arithmetic-Asian / partially fixed TWAP approximation +├── american_kbi.rs Fully on-chain Kim boundary integration for American options +├── american_kbi_data.rs Generated parameter-independent KBI quadrature geometry +├── heston.rs Exact deterministic Heston (`xi = 0`) reduction ├── sabr.rs SABR implied vol + pricing -├── nig.rs NIG fat-tail pricing (COS method) -├── i64_math.rs i64-scale NIG for on-chain use -├── i64_cf.rs i64-scale Heston characteristic function +├── nig.rs Bounded exponential-NIG OTM integration + parity +├── i64_math.rs Signed 1e6-scale NIG compatibility wrappers +├── i64_cf.rs Test-only Heston characteristic-function research ├── pool.rs Weighted pool swap + token conversion ├── bvn_cdf.rs Bivariate normal CDF -├── phi2table.rs Offline Φ₂ lookup-table generation +├── phi2table.rs Fixed-correlation Φ₂ tables and certificates +├── rainbow.rs Stulz best-of / worst-of two-asset calls └── lib.rs Feature-gated re-exports ``` @@ -169,9 +179,9 @@ core (always on) arithmetic, overflow, mul_div, double_word, encoding, constants, error transcendental (default) - transcendental, trig, normal, hp, i64_math + transcendental, trig, normal, hp -complex (default) +complex complex — requires transcendental bs @@ -183,11 +193,17 @@ iv barrier barrier — requires transcendental (uses HP internally) +asian + asian — requires transcendental (uses HP internally) + +american-kbi + american_kbi — requires transcendental + heston - heston, i64_cf — requires bs + complex + heston — requires bs nig - nig — requires transcendental + complex + nig, i64_math — requires transcendental sabr sabr — requires transcendental @@ -196,65 +212,104 @@ pool pool — requires transcendental (uses pow_fixed_hp) bivariate - bvn_cdf — requires transcendental + bvn_cdf, phi2table — requires transcendental + +rainbow + rainbow — requires bivariate table-gen - phi2table — requires bivariate + offline phi2table generation — requires bivariate + +pade-iv + alternate IV initializer — requires iv ``` -Default features: `transcendental + complex`. Use `features = ["full"]` -for production runtime modules, or `default-features = false` for core -arithmetic only. `table-gen`, `pade-iv`, and `idl-build` are explicit opt-ins. +The default feature is `transcendental`. Use `features = ["full"]` for all +stable runtime modules, or `default-features = false` for core arithmetic +only. Complex arithmetic and every pricing model are opt-in. Offline +`table-gen` and experimental `pade-iv` are explicit opt-ins and are +deliberately excluded from `full`. --- ## Validation assets -The crate ships compact reference assets for reproducibility and generated -tests: - -- `benchmark/iv_vectors.json` — compact implied-vol recovery vectors for regression validation -- `test_data/heston_reference_tests.rs` — generated Heston reference cases used by the crate test suite -- `test_data/sabr_reference_tests.rs` — generated SABR reference cases used by the crate test suite - -Larger generated corpora and Python generation scripts stay repository-only so -the crates.io package remains small. See `VALIDATION.md` for the exact split and -release commands. +The crate ships measured summaries in its model documentation. Machine-readable +reports, generated corpora, integration tests, SBF harnesses, and Python +generators stay repository-only so the crates.io package remains small. See +`VALIDATION.md` for the exact split and release commands. --- -## Polynomial approximation strategy +## Approximation strategy and table budget -All transcendental functions use **minimax (Remez) polynomials** fitted to -narrow subintervals after range reduction: +Transcendental functions begin with range reduction and use polynomial or +rational kernels on narrow subintervals. Small reduced-domain midpoint tables +are reserved for cases where the final SBF artifact demonstrates a material CU +improvement: | Function | Reduction | Polynomial | Domain after reduction | |----------|-----------|------------|-----------------------| -| `ln` | Binary shift to [1, 2) + 16-entry table lookup | Degree-3 arctanh Remez | `t ∈ [-1/32, 1/32]` | -| `exp` | Integer/fractional split via `k = round(x/ln2)` | Degree-5 Remez rational | `r ∈ [-ln2/2, ln2/2]` | +| `ln` / `ln1p` | Normalize to [1, 2) + 1,024 midpoint/reciprocal anchors | Q42 cubic correction | Half of one midpoint segment | +| `exp` | Octave + 32-way fractional split, division-free Q63 residual | Degree-5 Q22 minimax | `r ∈ [-ln2/64, ln2/64]` | +| `expm1` | Rounded `k·ln(2)` + 1,292 midpoint anchors | Q43 cubic correction | Half of one midpoint segment | | `sin`/`cos` | Cody-Waite `rem_euclid` to `(-π, π]` then octant | Degree-11/10 minimax | `x ∈ [-π/4, π/4]` | -| `norm_cdf` | 6-piece piecewise by `|x|` range | Degree-11 per piece | Per-piece mapped `t` | +| `norm_cdf` | Ten half-sigma body pieces + four half-sigma tails | Q23 degree 8/7 body; Q39-evaluated degree 6/5/4/3 tail | Q44 `t ∈ [-1, 1]` | The Remez coefficients are precomputed by offline Python scripts (in `scripts/`) -and baked into `constants.rs` as integer literals — no runtime fitting. +and baked into generated coefficient modules or `constants.rs` as integer +literals — no runtime fitting. +`ln`, `ln1p`, and `expm1` share a single rounded `k·ln(2)` table. The +`ln1p`+`expm1` combined raw payload is 27,944 bytes, down from 29,800 bytes +with duplicate reduction tables. + +The standard normal CDF is not table-driven. Its generated coefficient module +contains 936 bytes of coefficients/cutoff data and is compile-time capped at +2 KiB. Direct tail polynomials replace the former exponential, Mills-ratio +continued fraction, and divisions. An Arb/exact-integer certificate proves at +most 2 ULP, exact symmetry, integer safety, and nondecreasing output for every +`i128`; the Q39 tail guard uses the same stored Q23 coefficients and adds no +payload. + +The standard exponential is calculation-first rather than answer-table-driven: +six Q22 coefficients and 32 rounded Q62 fractional-power constants total 304 +bytes. Split-i64 Q63 reduction avoids wide division and the phase product is +rounded only once at final reconstruction. Its source-bound Arb/exact-integer +certificate proves monotonicity, relative error below 1.55×10^-16, and raw +error below 41,159 for `|x| < 20`; the retained 100K production corpus measured +33,622 max / 7,881 P99 / zero median. The deployed kernel measured 961 average / +992 max CU, and its isolated linked path is 21,272 bytes smaller than the +former rational implementation. + +The enforced optimization order is: + +1. range reduction and algebraic simplification; +2. Remez/minimax or another low-degree polynomial/rational kernel; +3. a small reduced-domain anchor table only after CU, accuracy, and linked SBF + measurements justify it. + +Compile-time assertions cap the `exp` payload at 512 bytes, `expm1` at 16 KiB, +`ln1p` at 20 KiB, and the latter pair's combined payload at 32 KiB. The +repository-only SBF footprint harness caps the linked `exp` increase at 10 KiB +and `expm1`/`ln1p`/combined increases at 22/34/50 KiB respectively. It also +verifies that the hybrid `expm1` remains smaller than its former +calculation-heavy path. Run `scripts/measure_sbf_footprint.sh` after changing a +table or kernel. --- -## Heston three-path architecture - -`heston_price` selects one of three code paths based on input characteristics: - -1. **Degenerate** (`t=0`, `s=0`, or `k=0`) — returns intrinsic value. ~100 CU. +## Deterministic Heston architecture -2. **BS fallback** (`ξ²T < 0.01`) — when vol-of-vol is negligible, the Heston - model reduces to Black-Scholes with effective variance `σ̄²`. Uses - `bs_full_hp`. ~130K CU. +`heston_price` has three public outcomes: -3. **Control-variate quadrature** — the full path. Computes a BS reference - price at an effective σ, then corrects it with a 21-node double-exponential - quadrature of the difference between the Heston and BS characteristic - functions. The Heston CF evaluates at `i64` scale (~1e6) for speed; the - BS CF evaluates at `i128`. ~410-430K CU. +1. **Expiry** (`t=0`) — intrinsic value. +2. **Deterministic variance** (`t>0`, `xi=0`) — an exact reduction using + cancellation-safe integrated CIR variance and HP Black-Scholes. The final + exp-final 2K SBF sample measured 118,523 CU average and 183,239 max; the + broader 2,004-case branch grid remains the conservative maximum at 190,756. +3. **Stochastic variance** (`t>0`, `xi>0`) — the public deterministic model + returns `NoConvergence`. Characteristic-function experiments remain confined + to test-only code and are not part of the runtime API. --- diff --git a/docs/ASIAN_TWAP.md b/docs/ASIAN_TWAP.md new file mode 100644 index 0000000..fed6b7c --- /dev/null +++ b/docs/ASIAN_TWAP.md @@ -0,0 +1,165 @@ +# Arithmetic-Asian / TWAP Option Pricing + +## Overview + +`arithmetic_asian_price` and its protocol-oriented alias +`twap_option_price` provide constant-time pricing for an option settled against +a continuous arithmetic average. The functions support an averaging window +that starts in the future and an average that is already partially fixed. + +SolMath computes the arithmetic average's first two GBM moments analytically +and prices their matched lognormal distribution, following the +Levy/Turnbull-Wakeman family of arithmetic-Asian approximations. + +## Settlement state + +The final average is + +```text +A = w A_fixed + (1-w) B +B = 1/tau integral_[T-tau,T] S(u) du +``` + +where: + +- `T` is `t`, the time remaining until payment; +- `tau` is `averaging_time`, the remaining continuous averaging-window length; +- `w` is `fixed_weight`, the fraction already observed; +- `A_fixed` is `fixed_average`, the average of those observations. + +For a 30-minute settlement TWAP: + +| State | `t` | `averaging_time` | `fixed_weight` | `fixed_average` | +|---|---:|---:|---:|---:| +| Before the window | time to expiry | 30 minutes | 0 | 0 | +| 12 minutes observed | 18 minutes | 18 minutes | 12/30 | observed 12-minute TWAP | +| Fully fixed | time to payment | 0 | 1 | final TWAP | + +Times are year fractions at `SCALE = 1e12`. Derive all four state fields from +one canonical observation accumulator so the quote and persisted fixing state +remain coherent. + +## Moment calculation + +Under risk-neutral GBM with carry `b = r - q`, let + +```text +a = T - tau +B0 = b tau +V = sigma^2 tau +phi1(x) = expm1(x) / x +``` + +Then the future average has exact first moment + +```text +E[B] = S exp(b a) phi1(B0) +``` + +and exact second moment + +```text +E[B^2] = S^2 exp((2b + sigma^2) a) J(B0,V) + +J(B0,V) = 2/B0 [exp(B0) phi1(B0+V) - phi1(2B0+V)]. +``` + +The implementation uses the continuous limit at `B0 = 0` and a bivariate +series near the origin, avoiding cancellation for minute-scale TWAP windows. +The fixed portion is deterministic at quote time, so + +```text +E[A] = w A_fixed + (1-w) E[B] +Var[A] = (1-w)^2 Var[B]. +``` + +The matched lognormal variance is + +```text +v = ln(1 + Var[A] / E[A]^2). +``` + +Calls and puts are priced from `(E[A], v)` and discounted to payment. The +public outputs satisfy discounted average put-call parity exactly after +fixed-point rounding. + +## API + +```rust +use solmath::{twap_option_price, SCALE}; + +let minutes = |n: u128| n * SCALE / (365 * 24 * 60); +let quote = twap_option_price( + 100 * SCALE, // spot + 100 * SCALE, // strike + 50_000_000_000, // r = 5% + 20_000_000_000, // q = 2% + 600_000_000_000, // sigma = 60% + minutes(18), // time to payment + minutes(18), // remaining averaging window + 99_500_000_000_000, // fixed average = 99.50 + 400_000_000_000, // fixed weight = 12/30 +)?; + +let _ = (quote.call, quote.put, quote.expected_average, quote.log_variance); +# Ok::<(), solmath::SolMathError>(()) +``` + +Use `TwapInputs::from_raw` at an instruction boundary to validate prices, +rates, volatility, times, the averaging-window relation, and fixing-state +coherence once. + +## Validation + +- Six committed 80-decimal mpmath vectors cover unseasoned, future-starting, + partially fixed, tiny-price, long-maturity, and 18-minute TWAP cases. +- `benchmark/prod_asian_vectors.json` contains exactly 100,000 stratified + production vectors, and `benchmark/adv_asian_vectors.json` contains exactly + 10,000 adversarial vectors spanning tiny windows, the moment-series seam, + raw carry/fixing seams, future starts, deep tails, high variance, and + near-ATM partially fixed contracts with very little residual variance. All + 110,000 compiled calls completed without rejection. Against 60-digit mpmath, production + call/put P99 was `1,180` raw and maximum was `22,580` raw (`$2.258e-8`); + adversarial call/put P99 was `1,220,288` raw and maximum was `19,587,949` + raw (`$0.000019587949`). The adversarial maximum has only `16` raw matched + log variance, so SCALE-resolution log/CDF inputs are strongly amplified at + the near-ATM transition. Generate and validate the corpora with + `scripts/generate_asian_vectors.py` and + `scripts/validate_asian_corpora.py`. +- `benchmark/asian_quantlib_vectors.json` commits 10,000 prices produced + directly by QuantLib 1.41 `ContinuousArithmeticAsianLevyEngine`; regenerate + it with `scripts/generate_asian_quantlib_vectors.py`. A 500-vector generated + subset runs under Cargo. Across all 10,000 rows the call/put median raw + deviations are `110/112`; maxima are `$0.000594854/$0.000594852` in a + cancellation-sensitive two-day QuantLib case. +- The optimized deployed composite SBF artifact was measured on 2,000 + practical runtime inputs: `137,997` average, `161,471` median, `177,422` + P95, `180,029` P99, and `182,458` maximum math CU. Every call succeeded. +- A separate 10,000-input full-domain/branch-seam sweep, including 3,332 + expected domain rejections, measured every call and maxed at `185,590` + math CU. Complete Anchor transactions maxed at `186,610` CU, leaving + `13,390` CU below the 200K target. The raw report is + `benchmark/asian_cu_report.json`. + +These checks validate the fixed-point implementation against the stated +moment-match model and characterize its behavior at the important fixed-point +and near-deterministic seams. + +Reproduce the randomized implementation check with: + +```bash +python3 scripts/validate_asian_runtime.py --cases 500 +python3 scripts/generate_asian_vectors.py +python3 scripts/validate_asian_corpora.py +python3 scripts/generate_asian_quantlib_vectors.py +``` + +## Model scope + +- Continuous sampling is an approximation to a discrete oracle feed. +- The model assumes GBM with constant `r`, `q`, and `sigma`. +- `fixed_average` and `fixed_weight` must come from persisted, authenticated + oracle state. +- Protocols with a discrete observation feed can compare their exact schedule + with Monte Carlo when calibrating the continuous-sampling approximation. +- No Greeks or implied-volatility inversion are included in the first version. diff --git a/docs/NIG.md b/docs/NIG.md new file mode 100644 index 0000000..3f8c568 --- /dev/null +++ b/docs/NIG.md @@ -0,0 +1,192 @@ +# Exponential NIG option pricing + +The `nig` feature provides a fully on-chain European call/put engine for the +exponential Normal Inverse Gaussian model. It accepts spot, strike, rates, +time, and NIG parameters; constructs the martingale correction and density; +evaluates the option integral; and returns both sides of put-call parity with a +quote-local numerical allowance. + +There is no live QuantLib call, uploaded matrix, sampled price surface, +operator account, or trusted off-chain builder. The embedded data consists of +parameter-independent quadrature geometry and scaled-Bessel approximation +coefficients. + +## Model + +For elapsed NIG scale `d = delta_per_year * T`, define + +```text +gamma = sqrt(alpha^2 - beta^2) +gamma1 = sqrt(alpha^2 - (beta + 1)^2) +k = ln(S/K) + (r-q)T + d(gamma1-gamma) +h = -k +``` + +The `gamma1-gamma` term is the exponential-moment correction that makes the +discounted asset a martingale. Moving to the stock numeraire shifts `beta` to +`beta + 1`. + +The engine integrates only the smaller out-of-the-money leg: + +```text +C_otm = K exp(-rT) integral_0^inf (exp(y)-1) f_beta(h+y) dy +P_otm = K exp(-rT) integral_0^inf (1-exp(-y)) f_beta(h-y) dy +``` + +The other leg follows from fixed-point put-call parity. This avoids subtracting +two large, nearly equal CDF terms. The density is evaluated as + +```text +f_beta(x) = alpha*d/(pi*omega) + * [exp(alpha*omega) K1(alpha*omega)] + * exp(d*gamma + beta*x - alpha*omega) +omega = hypot(d, x) +``` + +The scaled `exp(x) K1(x)` form prevents a large intermediate Bessel value. +Piecewise Chebyshev/asymptotic kernels are generated by +`scripts/generate_nig_k1_coeffs.py`; the canonical coefficient payload SHA-256 +is `77e439688b161d544e786a78cbd7256b1364e133abba217bc08b83e0145b60dc`. + +The half-line map `y = L*t/(1-t)` feeds an embedded Gauss–Kronrod 15 / Gauss 7 +pair. Deep out-of-the-money cases can use a parameter-derived Chernoff bound +and return through the cheaper tail tier. The tail tier is capped at `$0.005` +per `$100` of discounted notional regardless of a looser caller request. + +## API + +```rust +use solmath::{nig_price_certified, NigParams, SCALE}; + +let quote = nig_price_certified( + 100 * SCALE, // spot + 100 * SCALE, // strike + 50_000_000_000, // continuously compounded rate = 5% + 20_000_000_000, // continuous dividend yield = 2% + SCALE, // one year + NigParams { + alpha: 15 * SCALE, + beta: -2 * SCALE as i128, + delta_per_year: SCALE, + }, + 5_000_000_000, // requested absolute error = 0.005 +)?; + +let _ = (quote.call, quote.put, quote.max_abs_error, quote.tier); +# Ok::<(), solmath::SolMathError>(()) +``` + +```rust,ignore +pub struct CertifiedNigPrice { + pub call: u128, + pub put: u128, + pub max_abs_error: u128, + pub tier: u8, // 0 expiry, 1 Chernoff tail, 15 full quadrature +} +``` + +All fields use `SCALE = 1e12`. `alpha` and `beta` are inverse log-return units; +`delta_per_year` scales linearly with time. Compatibility functions +`nig_call_price`, `nig_call_64`, and `nig_put_64` use `q = 0` and the standard +`$0.005 / $100` request. + +Enable the engine independently: + +```toml +solmath = { version = "0.2", default-features = false, features = ["nig"] } +``` + +## Runtime domain + +| Quantity | Supported range | +|---|---:| +| `spot`, `strike` | `(0, 100,000]` | +| `time` | `(0, 5]` years | +| `rate`, `dividend_yield` | `[-0.25, 0.25]` | +| `alpha` | `[2, 100]` | +| `abs(beta) / alpha` | `<= 0.65` | +| `abs(beta + 1) / alpha` | `<= 0.65` | +| `delta_per_year` | `(0, 15]` | +| `delta_per_year * time` | `[0.001, 15]` | +| absolute log-forward moneyness | `<= 2` | + +At expiry the result is exact intrinsic value with tier `0`. Inputs outside the +runtime domain return `DomainError`; a computed allowance above the caller's +request returns `NoConvergence`. + +## Accuracy + +The release corpus compares the compiled Rust path with a high-precision +two-measure NIG CDF identity. The reference reflects upper tails to avoid +`1-CDF` cancellation and cross-checks difficult points with adaptive direct +density integration. + +| Corpus | Quotes | Priced | Rejected by runtime contract | Median error / $100 | P99 | Maximum | Allowance violations | +|---|---:|---:|---:|---:|---:|---:|---:| +| Production | 100,000 | 87,715 | 12,285 | `$4.02e-9` | `$1.73e-5` | `$0.000565278` | 0 | +| Adversarial | 10,000 | 6,898 | 3,102 | `$5.31e-9` | `$0.000170265` | `$0.001396550` | 0 | + +There were also zero violations of the caller's requested maximum error. The +production/adversarial input SHA-256 values are +`61cb4bd21a99928fde3fd0355d33ef48678bb45e4de342b7e1b534c70fdb097c` +and `8853de248219801b23eca772f3613caf011d1f8b0408ddd877645bb0c35e9340`. + +An independent 50-digit check uses direct Bessel-density integration and Lewis +characteristic-function inversion. Those two reference methods differed by at +most `1.775e-16` dollars; the fixed-point engine differed by at most +`$0.000103316` on the six independent-oracle regimes. + +`max_abs_error` combines the embedded-rule estimate with a fixed-point floor. +The release corpora and independent oracle support that allowance throughout +the measured domain; it is an empirical numerical contract rather than a +symbolic all-domain Gauss–Kronrod remainder theorem. + +## Compute and footprint + +The Agave 2.3.0 campaign executed 2,000 generated quotes against the current +composite SBF artifact: + +| Metric | Math CU | Full instruction CU | +|---|---:|---:| +| Median | 28,261 | — | +| P99 | 367,321 | 368,377 | +| Maximum | 381,385 | 382,441 | + +The inexpensive median is the Chernoff tail tier; full quadrature determines +the upper end. The measured composite artifact is `1,083,408` bytes with +SHA-256 `685d49886179ca0bec80e31d9e9b878f3d044a47869cfd2f70cc2cf194c05161`. + +In the isolated footprint harness, the Anchor baseline is `184,848` bytes and +the NIG variant is `311,464` bytes, a `126,616`-byte linked delta. + +## Reproduction + +```bash +python3 scripts/generate_nig_k1_coeffs.py +python3 scripts/nig_reference.py +python3 scripts/validate_nig_runtime.py --production 100000 --adversarial 10000 +NO_DNA=1 cargo build-sbf --manifest-path benchmark/sbf-composite/Cargo.toml +``` + +The machine-readable evidence lives in: + +- `benchmark/nig_release_report.json` +- `benchmark/nig_independent_oracle_report.json` +- `benchmark/nig_cu_report.json` +- `benchmark/nig_footprint_report.json` + +## Model scope + +- The engine prices European options under an exponential NIG Lévy process. +- Rates, yields, and NIG parameters are constant over the contract horizon. +- Quotes outside the published runtime domain are represented as errors rather + than extrapolated values. +- American exercise is handled separately by `american-kbi`. + +The model convention follows Barndorff-Nielsen's exponential NIG process. The +Esscher/CDF identity and independent inversion are consistent with the modern +NIG CDF and option-pricing literature: + +- +- +- diff --git a/examples/README.md b/examples/README.md index 02cfc77..715aec4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,10 @@ # SolMath Examples - `options_pricing.rs` — Black-Scholes price and Greeks with `fp("...")` input helpers. +- `twap_options.rs` — partially fixed 30-minute arithmetic-Asian/TWAP settlement; run with `--features asian`. +- `asian_batch.rs` — line-oriented offline validation harness; run with `--features asian`. +- `american_kbi_batch.rs` — line-oriented KBI runtime validator; run with `--features american-kbi`. +- `nig_batch.rs` — line-oriented exponential-NIG runtime validator; run with `--features nig`. - `weighted_pool_swap.rs` — Balancer-style weighted pool swap; run with `--features pool`. - `safe_token_conversion.rs` — floor/ceil token conversion policy; run with `--features pool`. - `anchor_options_pricing.md` — Anchor instruction template for option quoting. diff --git a/examples/american_kbi_batch.rs b/examples/american_kbi_batch.rs new file mode 100644 index 0000000..f6f2536 --- /dev/null +++ b/examples/american_kbi_batch.rs @@ -0,0 +1,42 @@ +//! Batch harness for validating the actual fixed-point KBI runtime. + +use std::io::{self, BufRead}; + +use solmath::{american_kbi_price, AmericanKbiKind}; + +fn main() { + for line in io::stdin().lock().lines() { + let line = line.expect("stdin read failed"); + let fields: Vec<_> = line.split_whitespace().collect(); + if fields.len() != 7 { + println!("ERR:fields"); + continue; + } + let kind = match fields[0] { + "call" => AmericanKbiKind::Call, + "put" => AmericanKbiKind::Put, + _ => { + println!("ERR:kind"); + continue; + } + }; + let mut values = [0u128; 6]; + let mut valid = true; + for (target, source) in values.iter_mut().zip(&fields[1..]) { + match source.parse::() { + Ok(value) => *target = value, + Err(_) => valid = false, + } + } + if !valid { + println!("ERR:number"); + continue; + } + match american_kbi_price( + values[0], values[1], values[2], values[3], values[4], values[5], kind, + ) { + Ok(price) => println!("{price}"), + Err(error) => println!("ERR:{error:?}"), + } + } +} diff --git a/examples/anchor_options_pricing.md b/examples/anchor_options_pricing.md index 43d4682..ecf9cbb 100644 --- a/examples/anchor_options_pricing.md +++ b/examples/anchor_options_pricing.md @@ -8,8 +8,8 @@ program receives already-validated `u128` values scaled by `SCALE = 1e12`. ```toml [dependencies] -anchor-lang = { version = "0.31", features = ["init-if-needed"] } -solmath = { version = "0.1", default-features = false, features = ["transcendental"] } +anchor-lang = { version = "0.32", features = ["init-if-needed"] } +solmath = { version = "0.2", default-features = false, features = ["transcendental"] } ``` Instruction code: diff --git a/examples/asian_batch.rs b/examples/asian_batch.rs new file mode 100644 index 0000000..6bda6d1 --- /dev/null +++ b/examples/asian_batch.rs @@ -0,0 +1,39 @@ +//! Line-oriented batch harness for offline Asian/TWAP validation. +//! +//! Each input line contains nine raw `u128` values: +//! `s k r q sigma t averaging_time fixed_average fixed_weight`. + +use std::io::{self, BufRead}; + +use solmath::arithmetic_asian_price; + +fn main() { + for line in io::stdin().lock().lines() { + let Ok(line) = line else { + println!("ERR input"); + continue; + }; + let values = line + .split_whitespace() + .map(str::parse::) + .collect::, _>>(); + let Ok(values) = values else { + println!("ERR parse"); + continue; + }; + if values.len() != 9 { + println!("ERR arity"); + continue; + } + match arithmetic_asian_price( + values[0], values[1], values[2], values[3], values[4], values[5], values[6], values[7], + values[8], + ) { + Ok(result) => println!( + "{} {} {} {}", + result.call, result.put, result.expected_average, result.log_variance + ), + Err(error) => println!("ERR {error}"), + } + } +} diff --git a/examples/nig_batch.rs b/examples/nig_batch.rs new file mode 100644 index 0000000..2fcdabc --- /dev/null +++ b/examples/nig_batch.rs @@ -0,0 +1,53 @@ +//! Batch harness for validating the actual fixed-point exponential-NIG runtime. + +use std::io::{self, BufRead}; + +use solmath::{nig_price_certified, NigParams}; + +fn main() { + for line in io::stdin().lock().lines() { + let line = line.expect("stdin read failed"); + let fields: Vec<_> = line.split_whitespace().collect(); + if fields.len() != 9 { + println!("ERR:fields"); + continue; + } + + let unsigned = [0usize, 1, 4, 5, 7, 8]; + let mut u = [0u128; 6]; + let mut valid = true; + for (target, index) in u.iter_mut().zip(unsigned) { + match fields[index].parse::() { + Ok(value) => *target = value, + Err(_) => valid = false, + } + } + let rate = fields[2].parse::(); + let dividend = fields[3].parse::(); + let beta = fields[6].parse::(); + if !valid || rate.is_err() || dividend.is_err() || beta.is_err() { + println!("ERR:number"); + continue; + } + + match nig_price_certified( + u[0], + u[1], + rate.unwrap(), + dividend.unwrap(), + u[2], + NigParams { + alpha: u[3], + beta: beta.unwrap(), + delta_per_year: u[4], + }, + u[5], + ) { + Ok(quote) => println!( + "OK {} {} {} {}", + quote.call, quote.put, quote.max_abs_error, quote.tier + ), + Err(error) => println!("ERR:{error:?}"), + } + } +} diff --git a/examples/twap_options.rs b/examples/twap_options.rs new file mode 100644 index 0000000..c29808b --- /dev/null +++ b/examples/twap_options.rs @@ -0,0 +1,27 @@ +//! Price an option settled against a partially fixed 30-minute TWAP. + +use solmath::{twap_option_price, SCALE}; + +fn main() -> Result<(), solmath::SolMathError> { + let minutes = |value: u128| value * SCALE / (365 * 24 * 60); + + // Twelve minutes of a thirty-minute settlement TWAP have already fixed at + // $99.50. Eighteen minutes remain until expiry. + let result = twap_option_price( + 100 * SCALE, // current spot + 100 * SCALE, // strike + 50_000_000_000, // risk-free rate: 5% + 20_000_000_000, // continuous yield: 2% + 600_000_000_000, // volatility: 60% + minutes(18), // time to expiry + minutes(18), // remaining averaging time + 99_500_000_000_000, // fixed average: $99.50 + 400_000_000_000, // fixed weight: 12 / 30 + )?; + + println!("call: {}", result.call); + println!("put: {}", result.put); + println!("expected average: {}", result.expected_average); + println!("matched log-variance: {}", result.log_variance); + Ok(()) +} diff --git a/scripts/american_kbi_reference.py b/scripts/american_kbi_reference.py new file mode 100644 index 0000000..7d142ce --- /dev/null +++ b/scripts/american_kbi_reference.py @@ -0,0 +1,1032 @@ +#!/usr/bin/env python3 +"""Test a Kim Boundary Integration pricer against QuantLib QdFp. + +The experiment deliberately mirrors the constraints of the intended Solana +implementation: + +* only the six option inputs are live inputs; +* one normalized American-put kernel prices both puts and calls (by duality); +* the exercise boundary is reconstructed from Kim's nonlinear boundary equation; +* the final price is the European value plus the early-exercise premium. + +This first implementation is a floating-point accuracy oracle. It reports the +number of CDF/kernel evaluations as well as price errors so that numerically +accurate configurations which cannot plausibly meet the CU budget are visible +immediately. Composite Gauss-Legendre integration is used on a graded +time-to-expiry mesh; boundary values inside each interval are interpolated +linearly, including the as-yet unknown right endpoint during each scalar solve. +""" + +from __future__ import annotations + +import argparse +import json +import math +import pathlib +from dataclasses import asdict, dataclass + +import numpy as np +from scipy.optimize import brentq +from scipy.special import ndtr + +import american_quantlib_reference as base + + +@dataclass +class Work: + """Operation counts for a boundary construction and a single price.""" + + residual_evaluations: int = 0 + boundary_kernel_points: int = 0 + price_kernel_points: int = 0 + + @property + def cdf_evaluations(self) -> int: + # Each kernel point evaluates both d1 and d2. + return 2 * (self.boundary_kernel_points + self.price_kernel_points) + + +@dataclass(frozen=True) +class Boundary: + times: np.ndarray + values: np.ndarray + work: Work + + +def european_put(spot: np.ndarray | float, maturity: float, r: float, q: float, + sigma: float) -> np.ndarray | float: + """Normalized Black-Scholes-Merton put with strike one.""" + x = np.asarray(spot, dtype=np.float64) + if maturity <= 0.0: + result = np.maximum(1.0 - x, 0.0) + else: + root_t = math.sqrt(maturity) + standard_deviation = sigma * root_t + d1 = (np.log(x) + (r - q + 0.5 * sigma * sigma) * maturity) / standard_deviation + d2 = d1 - standard_deviation + result = math.exp(-r * maturity) * ndtr(-d2) - x * math.exp(-q * maturity) * ndtr(-d1) + if np.ndim(spot) == 0: + return float(result) + return result + + +def expiry_boundary(r: float, q: float) -> float: + """Right limit B(0+)/K of the put exercise boundary.""" + if q > r and q > 0.0: + return max(r / q, 0.0) + return 1.0 + + +def hermite_norm_cdf_pdf( + value: np.ndarray | float, step: float +) -> tuple[np.ndarray | float, np.ndarray | float]: + """Cubic-Hermite normal CDF and its analytic derivative on a fixed grid.""" + x = np.asarray(value, dtype=np.float64) + absolute = np.abs(x) + clipped = np.minimum(absolute, 8.0) + intervals = round(8.0 / step) + index = np.minimum((clipped / step).astype(np.int64), intervals - 1) + left = index * step + coordinate = (clipped - left) / step + y0 = ndtr(left) + y1 = ndtr(left + step) + inverse_root_two_pi = 1.0 / math.sqrt(2.0 * math.pi) + p0 = inverse_root_two_pi * np.exp(-0.5 * left * left) + p1 = inverse_root_two_pi * np.exp(-0.5 * (left + step) ** 2) + a = 2.0 * y0 - 2.0 * y1 + step * (p0 + p1) + b = -3.0 * y0 + 3.0 * y1 - step * (2.0 * p0 + p1) + c = step * p0 + cdf_positive = ((a * coordinate + b) * coordinate + c) * coordinate + y0 + pdf = ((3.0 * a * coordinate + 2.0 * b) * coordinate + c) / step + cdf = np.where(x >= 0.0, cdf_positive, 1.0 - cdf_positive) + cdf = np.where(x > 8.0, 1.0, np.where(x < -8.0, 0.0, cdf)) + pdf = np.where(absolute > 8.0, 0.0, pdf) + if np.ndim(value) == 0: + return float(cdf), float(pdf) + return cdf, pdf + + +class KimBoundaryIntegration: + """Product-in-time discretization of Kim's boundary equation.""" + + def __init__( + self, + nodes: int, + quadrature_order: int, + grading: float, + product_basis: str = "linear-t", + fh_degree: int = 3, + root_solver: str = "brent", + newton_steps: int = 4, + price_mesh: str = "boundary", + price_power: float = 2.0, + late_newton_steps: int | None = None, + newton_cutover: int = 0, + derivative_mode: str = "full", + price_boundary_interp: str = "linear", + boundary_normal: str = "exact", + price_normal: str = "exact", + collocation_stride: int = 1, + boundary_order: int = 4, + price_order: int = 12, + third_predictor_alpha: float = 1.0, + ) -> None: + if nodes < 2 or quadrature_order < 1 or grading < 1.0: + raise ValueError("nodes >= 2, quadrature-order >= 1, grading >= 1 required") + self.nodes = nodes + self.quadrature_order = quadrature_order + self.grading = grading + self.product_basis = product_basis + self.fh_degree = fh_degree + self.root_solver = root_solver + self.newton_steps = newton_steps + self.price_mesh = price_mesh + self.price_power = price_power + self.late_newton_steps = late_newton_steps + self.newton_cutover = newton_cutover + self.derivative_mode = derivative_mode + self.price_boundary_interp = price_boundary_interp + self.boundary_normal = boundary_normal + self.price_normal = price_normal + self.collocation_stride = collocation_stride + self.boundary_order = boundary_order + self.price_order = price_order + self.third_predictor_alpha = third_predictor_alpha + self.derivative_samples: list[tuple[float, ...]] = [] + self.gauss_x, self.gauss_w = np.polynomial.legendre.leggauss(quadrature_order) + + @staticmethod + def _kernel( + current_boundary: float, + lag: np.ndarray, + past_boundary: np.ndarray, + r: float, + q: float, + sigma: float, + ) -> np.ndarray: + root_lag = np.sqrt(lag) + standard_deviation = sigma * root_lag + d1 = ( + np.log(current_boundary / past_boundary) + + (r - q + 0.5 * sigma * sigma) * lag + ) / standard_deviation + d2 = d1 - standard_deviation + return ( + r * np.exp(-r * lag) * ndtr(-d2) + - q * current_boundary * np.exp(-q * lag) * ndtr(-d1) + ) + + def _prefix_quadrature( + self, + times: np.ndarray, + values: np.ndarray, + right_index: int, + candidate: float, + ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """Quadrature lags, weights and B(s) for [0, times[right_index]].""" + left = times[:right_index] + right = times[1:right_index + 1] + half_width = 0.5 * (right - left) + midpoint = 0.5 * (right + left) + sample_times = midpoint[:, None] + half_width[:, None] * self.gauss_x[None, :] + weights = half_width[:, None] * self.gauss_w[None, :] + + right_values = np.concatenate((values[1:right_index], np.asarray([candidate]))) + fraction = (sample_times - left[:, None]) / (right - left)[:, None] + boundary_samples = ( + values[:right_index, None] + + fraction * (right_values[:, None] - values[:right_index, None]) + ) + lag = times[right_index] - sample_times + return lag.ravel(), weights.ravel(), boundary_samples.ravel() + + def boundary(self, maturity: float, r: float, q: float, sigma: float) -> Boundary: + if maturity <= 0.0: + work = Work() + value = expiry_boundary(r, q) + return Boundary(np.asarray([0.0]), np.asarray([value]), work) + coordinate = np.linspace(0.0, 1.0, self.nodes + 1) + times = maturity * coordinate ** self.grading + values = np.empty(self.nodes + 1, dtype=np.float64) + values[0] = expiry_boundary(r, q) + work = Work() + + # With no interest-rate benefit from receiving the strike early, the + # normalized put has no non-trivial stopping region. Keep this exact + # branch out of the ill-conditioned boundary equation. + if r <= 1.0e-14: + values.fill(0.0) + return Boundary(times, values, work) + + for index in range(1, self.nodes + 1): + t = float(times[index]) + + def residual(candidate: float) -> float: + lag, weights, boundary_samples = self._prefix_quadrature( + times, values, index, candidate + ) + integral = float(np.dot( + weights, + self._kernel(candidate, lag, boundary_samples, r, q, sigma), + )) + work.residual_evaluations += 1 + work.boundary_kernel_points += lag.size + return 1.0 - candidate - european_put(candidate, t, r, q, sigma) - integral + + upper = min(float(values[index - 1]), expiry_boundary(r, q)) + upper = max(upper, 1.0e-12) + # The integral equation can be nearly tangent at short maturities. + # Search from the economically relevant upper branch downwards and + # take the first genuine sign change. + probes = np.concatenate(( + upper * (1.0 - np.geomspace(1.0e-11, 0.35, 28)), + np.geomspace(max(upper * 0.65, 1.0e-10), 1.0e-10, 28), + )) + probes = np.unique(np.clip(probes, 1.0e-12, upper))[::-1] + probe_values = [residual(float(point)) for point in probes] + root: float | None = None + for high_index in range(len(probes) - 1): + high = float(probes[high_index]) + low = float(probes[high_index + 1]) + f_high = probe_values[high_index] + f_low = probe_values[high_index + 1] + if f_high == 0.0: + root = high + break + if f_high * f_low < 0.0: + root = brentq(residual, low, high, xtol=2.0e-13, rtol=2.0e-13) + break + if root is None: + best = int(np.argmin(np.abs(probe_values))) + if abs(probe_values[best]) > 2.0e-7: + raise RuntimeError( + f"failed to bracket boundary at node {index}/{self.nodes}: " + f"t={t:.8g}, best B={probes[best]:.8g}, residual={probe_values[best]:.3g}" + ) + root = float(probes[best]) + values[index] = min(root, upper) + return Boundary(times, values, work) + + def _full_quadrature(self, boundary: Boundary) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + index = boundary.times.size - 1 + if self.price_mesh == "boundary": + return self._prefix_quadrature( + boundary.times, boundary.values, index, float(boundary.values[-1]) + ) + maturity = float(boundary.times[-1]) + if self.price_mesh == "global": + gauss_x, gauss_w = np.polynomial.legendre.leggauss(self.price_order) + y = 0.5 * (gauss_x + 1.0) + unit_weights = 0.5 * gauss_w + lag = maturity * y ** self.price_power + flat_samples = maturity - lag + weights = ( + maturity + * self.price_power + * y ** (self.price_power - 1.0) + * unit_weights + ) + else: + flat_samples = None + coordinate = np.linspace(0.0, 1.0, index + 1) + if self.price_mesh == "global": + pass + elif self.price_mesh == "uniform": + normalized = coordinate + elif self.price_mesh == "valuation": + normalized = 1.0 - (1.0 - coordinate) ** self.price_power + elif self.price_mesh == "double": + normalized = 0.5 * (1.0 - np.cos(math.pi * coordinate)) + else: + raise ValueError(f"unsupported price mesh: {self.price_mesh}") + if self.price_mesh != "global": + price_times = maturity * normalized + left = price_times[:-1] + right = price_times[1:] + half_width = 0.5 * (right - left) + midpoint = 0.5 * (right + left) + samples = midpoint[:, None] + half_width[:, None] * self.gauss_x[None, :] + weights = half_width[:, None] * self.gauss_w[None, :] + flat_samples = samples.ravel() + boundary_interval = np.searchsorted( + boundary.times, flat_samples, side="right" + ) - 1 + boundary_interval = np.clip(boundary_interval, 0, index - 1) + interval_left = boundary.times[boundary_interval] + interval_right = boundary.times[boundary_interval + 1] + fraction = (flat_samples - interval_left) / (interval_right - interval_left) + if self.price_boundary_interp == "log": + log_values = np.log(boundary.values) + boundary_samples = np.exp( + log_values[boundary_interval] + + fraction + * (log_values[boundary_interval + 1] - log_values[boundary_interval]) + ) + else: + boundary_samples = ( + boundary.values[boundary_interval] + + fraction + * (boundary.values[boundary_interval + 1] - boundary.values[boundary_interval]) + ) + return maturity - flat_samples, weights.ravel(), boundary_samples + + def put_prices( + self, + spots: np.ndarray, + maturity: float, + r: float, + q: float, + sigma: float, + boundary: Boundary | None = None, + ) -> tuple[np.ndarray, Boundary, int]: + spots = np.asarray(spots, dtype=np.float64) + if maturity <= 0.0: + b = boundary or self.boundary(maturity, r, q, sigma) + return np.maximum(1.0 - spots, 0.0), b, 0 + if r <= 1.0e-14: + b = boundary or self.boundary(maturity, r, q, sigma) + return np.asarray(european_put(spots, maturity, r, q, sigma)), b, 0 + b = boundary or self.boundary(maturity, r, q, sigma) + lag, weights, boundary_samples = self._full_quadrature(b) + root_lag = np.sqrt(lag)[None, :] + standard_deviation = sigma * root_lag + spot_matrix = spots[:, None] + d1 = ( + np.log(spot_matrix / boundary_samples[None, :]) + + (r - q + 0.5 * sigma * sigma) * lag[None, :] + ) / standard_deviation + d2 = d1 - standard_deviation + if self.price_normal == "hermite25": + cdf_d2, _ = hermite_norm_cdf_pdf(-d2, 0.25) + cdf_d1, _ = hermite_norm_cdf_pdf(-d1, 0.25) + else: + cdf_d2 = ndtr(-d2) + cdf_d1 = ndtr(-d1) + rate_premium = r * np.sum( + weights[None, :] * np.exp(-r * lag)[None, :] * cdf_d2, axis=1 + ) + dividend_premium = q * spots * np.sum( + weights[None, :] * np.exp(-q * lag)[None, :] * cdf_d1, axis=1 + ) + prices = np.asarray(european_put(spots, maturity, r, q, sigma)) + rate_premium - dividend_premium + intrinsic = np.maximum(1.0 - spots, 0.0) + # The premium representation is a continuation value. Apply the + # stopping decision at valuation time exactly. + prices = np.where(spots <= b.values[-1], intrinsic, prices) + prices = np.maximum(prices, intrinsic) + price_points = int(lag.size) + return prices, b, price_points + + +class SmoothPastingKbi(KimBoundaryIntegration): + """One-dimensional weakly-singular smooth-pasting reformulation. + + Differentiating the early-exercise-premium representation with respect to + spot and imposing delta = -1 removes the tangency of Kim's value-matching + residual. The remaining 1/sqrt(t-s) singularity is integrated exactly + against a piecewise-linear kernel. Thus one boundary residual uses one + kernel sample per time node rather than an inner normal-CDF quadrature. + """ + + @staticmethod + def _product_weights(times: np.ndarray, right_index: int) -> np.ndarray: + """Weights for integral g(s)/sqrt(t_i-s) ds with linear hat functions.""" + t = float(times[right_index]) + weights = np.zeros(right_index + 1, dtype=np.float64) + for interval in range(right_index): + a = float(times[interval]) + b = float(times[interval + 1]) + width = b - a + lag_a = t - a + lag_b = t - b + root_a = math.sqrt(lag_a) + root_b = math.sqrt(max(lag_b, 0.0)) + cubic_difference = lag_a * root_a - lag_b * root_b + root_difference = root_a - root_b + left_weight = ( + (2.0 / 3.0) * cubic_difference + - 2.0 * lag_b * root_difference + ) / width + right_weight = ( + 2.0 * lag_a * root_difference + - (2.0 / 3.0) * cubic_difference + ) / width + weights[interval] += left_weight + weights[interval + 1] += right_weight + return weights + + @staticmethod + def _trapezoid_weights(times: np.ndarray, right_index: int) -> np.ndarray: + weights = np.zeros(right_index + 1, dtype=np.float64) + widths = np.diff(times[:right_index + 1]) + weights[:-1] += 0.5 * widths + weights[1:] += 0.5 * widths + return weights + + @staticmethod + def _floater_hormann_weights(nodes: np.ndarray, degree: int) -> np.ndarray: + """Barycentric weights for a pole-free Floater-Hormann interpolant.""" + count = nodes.size + degree = min(max(degree, 0), count - 1) + weights = np.zeros(count, dtype=np.float64) + for k in range(count): + first = max(0, k - degree) + last = min(k, count - degree - 1) + total = 0.0 + for start in range(first, last + 1): + product = 1.0 + for j in range(start, start + degree + 1): + if j != k: + product /= abs(float(nodes[k] - nodes[j])) + total += product + weights[k] = (-1.0 if (k - degree) % 2 else 1.0) * total + scale = float(np.max(np.abs(weights))) + return weights / scale + + @classmethod + def _floater_hormann_basis( + cls, nodes: np.ndarray, samples: np.ndarray, degree: int + ) -> np.ndarray: + weights = cls._floater_hormann_weights(nodes, degree) + output = np.empty((samples.size, nodes.size), dtype=np.float64) + for row, sample in enumerate(samples): + difference = sample - nodes + exact = np.flatnonzero(np.abs(difference) <= 4.0e-15) + if exact.size: + output[row].fill(0.0) + output[row, exact[0]] = 1.0 + else: + terms = weights / difference + output[row] = terms / float(np.sum(terms)) + return output + + def _rational_product_weights( + self, times: np.ndarray, right_index: int + ) -> tuple[np.ndarray, np.ndarray]: + """Precomputable FH weights in time or the uniform graded coordinate.""" + if self.product_basis == "fh-u": + nodes_coordinate = np.arange(right_index + 1, dtype=np.float64) / self.nodes + right_coordinate = right_index / self.nodes + else: + nodes_coordinate = times[:right_index + 1] / times[-1] + right_coordinate = float(nodes_coordinate[-1]) + degree = min(self.fh_degree, right_index) + # These are coefficient-generation quadratures, not live pricing work. + # The resulting normalized weights are fixed constants for a chosen + # node count, grading exponent and FH degree. + x, w = np.polynomial.legendre.leggauss(128) + unit = 0.5 * (x + 1.0) + unit_weights = 0.5 * w + maturity_at_node = float(times[right_index]) + + if self.product_basis == "fh-u": + regular_coordinate = right_coordinate * unit ** (1.0 / self.grading) + singular_coordinate = ( + right_coordinate * (1.0 - unit * unit) ** (1.0 / self.grading) + ) + else: + regular_coordinate = right_coordinate * unit + singular_coordinate = right_coordinate * (1.0 - unit * unit) + regular_basis = self._floater_hormann_basis( + nodes_coordinate, regular_coordinate, degree + ) + regular_weights = ( + maturity_at_node * unit_weights @ regular_basis + ) + + # s/t = 1-y^2 exactly cancels the endpoint square-root singularity: + # ds/sqrt(t-s) = 2 sqrt(t) dy. + singular_basis = self._floater_hormann_basis( + nodes_coordinate, singular_coordinate, degree + ) + singular_weights = ( + 2.0 * math.sqrt(maturity_at_node) * unit_weights @ singular_basis + ) + return singular_weights, regular_weights + + def boundary(self, maturity: float, r: float, q: float, sigma: float) -> Boundary: + if maturity <= 0.0: + work = Work() + value = expiry_boundary(r, q) + return Boundary(np.asarray([0.0]), np.asarray([value]), work) + coordinate = np.linspace(0.0, 1.0, self.nodes + 1) + times = maturity * coordinate ** self.grading + values = np.empty(self.nodes + 1, dtype=np.float64) + values[0] = expiry_boundary(r, q) + work = Work() + if r <= 1.0e-14: + values.fill(0.0) + return Boundary(times, values, work) + + inverse_sigma_root_two_pi = 1.0 / (sigma * math.sqrt(2.0 * math.pi)) + for index in range(1, self.nodes + 1): + t = float(times[index]) + if self.product_basis == "gauss-y": + # lag=t*y^2 removes the 1/sqrt(lag) endpoint singularity. + # A single global Gaussian rule then serves both the regular + # and singular integrals with parameter-independent geometry. + gauss_x, gauss_w = np.polynomial.legendre.leggauss( + self.boundary_order + ) + y = 0.5 * (gauss_x + 1.0) + unit_weights = 0.5 * gauss_w + lag_samples = t * y * y + sample_times = t - lag_samples + sample_left = np.searchsorted( + times[:index + 1], sample_times, side="right" + ) - 1 + sample_left = np.clip(sample_left, 0, index - 1) + sample_right = sample_left + 1 + interpolation_fraction = ( + (sample_times - times[sample_left]) + / (times[sample_right] - times[sample_left]) + ) + singular_weights = 2.0 * math.sqrt(t) * unit_weights + regular_weights = 2.0 * t * y * unit_weights + elif self.product_basis in ("fh-u", "fh-t"): + singular_weights, regular_weights = self._rational_product_weights( + times, index + ) + else: + singular_weights = self._product_weights(times, index) + regular_weights = self._trapezoid_weights(times, index) + + def residual_and_derivative(candidate: float) -> tuple[float, float]: + coefficient_override = None + coefficient_derivative_override = None + if self.product_basis == "gauss-y": + augmented = np.concatenate( + (values[:index], np.asarray([candidate])) + ) + left_values = augmented[sample_left] + right_values = augmented[sample_right] + fraction = interpolation_fraction + if self.price_boundary_interp == "log": + log_past = ( + np.log(left_values) + + fraction + * (np.log(right_values) - np.log(left_values)) + ) + past = np.exp(log_past) + past_derivative = np.where( + sample_right == index, + fraction * past / candidate, + 0.0, + ) + node_coefficients = q - r / augmented + coefficient_override = ( + node_coefficients[sample_left] + + fraction + * ( + node_coefficients[sample_right] + - node_coefficients[sample_left] + ) + ) + coefficient_derivative_override = np.where( + sample_right == index, + fraction * r / (candidate * candidate), + 0.0, + ) + else: + past = left_values + fraction * ( + right_values - left_values + ) + past_derivative = np.where( + sample_right == index, fraction, 0.0 + ) + lag = lag_samples + inverse_standard_deviation = 1.0 / ( + sigma * np.sqrt(lag) + ) + d1 = ( + np.log(candidate / past) + + (r - q + 0.5 * sigma * sigma) * lag + ) * inverse_standard_deviation + d_candidate = ( + 1.0 / candidate - past_derivative / past + ) * inverse_standard_deviation + else: + lag = t - times[:index + 1] + past = np.concatenate((values[:index], np.asarray([candidate]))) + d1 = np.zeros(index + 1, dtype=np.float64) + d_candidate = np.zeros(index + 1, dtype=np.float64) + positive = lag > 0.0 + d1[positive] = ( + np.log(candidate / past[positive]) + + (r - q + 0.5 * sigma * sigma) * lag[positive] + ) / (sigma * np.sqrt(lag[positive])) + d_candidate[positive] = 1.0 / ( + candidate * sigma * np.sqrt(lag[positive]) + ) + past_derivative = np.zeros(index + 1, dtype=np.float64) + past_derivative[-1] = 1.0 + inverse_root_two_pi = 1.0 / math.sqrt(2.0 * math.pi) + discount_q = np.exp(-q * lag) + if self.boundary_normal == "exact": + cdf_minus_d1 = ndtr(-d1) + density_d1 = inverse_root_two_pi * np.exp(-0.5 * d1 * d1) + else: + step = 0.25 if self.boundary_normal == "hermite25" else 0.5 + cdf_minus_d1, density_d1 = hermite_norm_cdf_pdf(-d1, step) + regular = discount_q * cdf_minus_d1 + # S e^-q tau phi(d1) = K e^-r tau phi(d2). + # Here S=candidate and K=past, so the two density kernels + # collapse to one shared phi(d1) evaluation. + coefficient = ( + coefficient_override + if coefficient_override is not None + else q - r / past + ) + singular = discount_q * density_d1 * coefficient + regular_derivative = ( + -discount_q * density_d1 * d_candidate + ) + singular_derivative = ( + discount_q + * density_d1 + * ( + -coefficient * d1 * d_candidate + + ( + coefficient_derivative_override + if coefficient_derivative_override is not None + else (r / (past * past)) * past_derivative + ) + ) + ) + if self.derivative_mode == "diagonal" or self.derivative_mode.startswith("last"): + retained = 0 if self.derivative_mode == "diagonal" else int( + self.derivative_mode.removeprefix("last") + ) + cutoff = max(0, index - retained) + regular_derivative[:cutoff] = 0.0 + singular_derivative[:cutoff] = 0.0 + d1_european = ( + math.log(candidate) + + (r - q + 0.5 * sigma * sigma) * t + ) / (sigma * math.sqrt(t)) + work.residual_evaluations += 1 + work.boundary_kernel_points += lag.size + if self.boundary_normal == "exact": + european_cdf = float(ndtr(-d1_european)) + european_pdf = math.exp(-0.5 * d1_european * d1_european) * inverse_root_two_pi + else: + step = 0.25 if self.boundary_normal == "hermite25" else 0.5 + european_cdf, european_pdf = hermite_norm_cdf_pdf(-d1_european, step) + value = ( + 1.0 + - math.exp(-q * t) * european_cdf + - q * float(np.dot(regular_weights, regular)) + + (1.0 / sigma) * float(np.dot(singular_weights, singular)) + ) + european_derivative = ( + math.exp(-q * t) + * european_pdf + / (candidate * sigma * math.sqrt(t)) + ) + derivative = ( + european_derivative + - q * float(np.dot(regular_weights, regular_derivative)) + + (1.0 / sigma) + * float(np.dot(singular_weights, singular_derivative)) + ) + self.derivative_samples.append( + ( + float(index), + 2.0 * r / (sigma * sigma), + 2.0 * q / (sigma * sigma), + sigma * sigma * float(times[-1]), + candidate, + value, + derivative, + european_derivative, + ) + ) + if self.derivative_mode.startswith("euro"): + derivative = european_derivative * float( + self.derivative_mode.removeprefix("euro") + ) + return value, derivative + + def residual(candidate: float) -> float: + return residual_and_derivative(candidate)[0] + + upper = max(min(float(values[index - 1]), expiry_boundary(r, q)), 1.0e-12) + floor = min(1.0e-10, 0.5 * upper) + if self.root_solver == "newton": + # The graded mesh is uniform in sqrt(time), where log B is + # nearly affine. Sparse defect correction can therefore + # retain the full interpolation mesh while avoiding a dense + # nonlinear residual at every node. + if ( + self.collocation_stride > 1 + and index > 3 + and index < self.nodes + and index % self.collocation_stride != 0 + ): + values[index] = min( + values[index - 1] * values[index - 1] / values[index - 2], + upper, + ) + continue + if index == 1: + asymptotic_scale = 2.0 if values[0] > 0.95 else 0.75 + candidate = upper * math.exp( + -asymptotic_scale * sigma * math.sqrt(t) + ) + else: + extrapolated = ( + values[index - 1] * values[index - 1] / values[index - 2] + ) + if index == 3: + candidate = values[index - 1] + self.third_predictor_alpha * ( + extrapolated - values[index - 1] + ) + else: + candidate = extrapolated + candidate = min(max(candidate, floor), upper * (1.0 - 1.0e-12)) + steps_here = self.newton_steps + if ( + self.late_newton_steps is not None + and index > self.newton_cutover + ): + steps_here = self.late_newton_steps + for _ in range(steps_here): + value, derivative = residual_and_derivative(candidate) + if abs(value) <= 2.0e-12: + break + if not math.isfinite(derivative) or abs(derivative) < 1.0e-12: + proposal = 0.5 * candidate + else: + proposal = candidate - value / derivative + # Safeguard against a bad short-time asymptotic predictor + # while retaining quadratic convergence near the root. + proposal = min(proposal, upper * (1.0 - 1.0e-12)) + proposal = max(proposal, floor) + if proposal < 0.35 * candidate: + proposal = 0.35 * candidate + elif proposal > 1.65 * candidate: + proposal = 1.65 * candidate + candidate = proposal + values[index] = min(candidate, upper) + continue + f_upper = residual(upper) + f_floor = residual(floor) + if f_upper == 0.0: + values[index] = upper + continue + if f_upper * f_floor < 0.0: + values[index] = brentq( + residual, floor, upper, xtol=2.0e-13, rtol=2.0e-13 + ) + continue + probes = np.concatenate(( + upper * (1.0 - np.geomspace(1.0e-11, 0.45, 32)), + np.geomspace(max(upper * 0.55, 1.0e-10), 1.0e-10, 32), + )) + probes = np.unique(np.clip(probes, 1.0e-12, upper))[::-1] + probe_values = [residual(float(point)) for point in probes] + root: float | None = None + for high_index in range(len(probes) - 1): + high = float(probes[high_index]) + low = float(probes[high_index + 1]) + f_high = probe_values[high_index] + f_low = probe_values[high_index + 1] + if f_high == 0.0: + root = high + break + if f_high * f_low < 0.0: + root = brentq(residual, low, high, xtol=2.0e-13, rtol=2.0e-13) + break + if root is None: + best = int(np.argmin(np.abs(probe_values))) + raise RuntimeError( + f"smooth-pasting boundary root missing at node {index}/{self.nodes}: " + f"t={t:.8g}, best B={probes[best]:.8g}, residual={probe_values[best]:.3g}" + ) + values[index] = min(root, upper) + return Boundary(times, values, work) + + +def metric(values: list[float]) -> dict[str, float | int]: + array = np.asarray(values, dtype=np.float64) + return { + "count": int(array.size), + "median": float(np.median(array)), + "p95": float(np.quantile(array, 0.95)), + "p99": float(np.quantile(array, 0.99)), + "max": float(np.max(array)), + "mean": float(np.mean(array)), + } + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--nodes", type=int, default=16) + parser.add_argument("--quadrature-order", type=int, default=2) + parser.add_argument("--grading", type=float, default=2.0) + parser.add_argument( + "--boundary-equation", + choices=("value", "smooth"), + default="smooth", + ) + parser.add_argument( + "--product-basis", + choices=("linear-t", "fh-u", "fh-t", "gauss-y"), + default="linear-t", + ) + parser.add_argument("--fh-degree", type=int, default=3) + parser.add_argument( + "--root-solver", choices=("brent", "newton"), default="brent" + ) + parser.add_argument("--newton-steps", type=int, default=4) + parser.add_argument( + "--price-mesh", + choices=("boundary", "uniform", "valuation", "double", "global"), + default="boundary", + ) + parser.add_argument("--price-power", type=float, default=2.0) + parser.add_argument("--late-newton-steps", type=int) + parser.add_argument("--newton-cutover", type=int, default=0) + parser.add_argument( + "--derivative-mode", + default="full", + ) + parser.add_argument( + "--price-boundary-interp", + choices=("linear", "log"), + default="linear", + ) + parser.add_argument( + "--boundary-normal", + choices=("exact", "hermite25", "hermite50"), + default="exact", + ) + parser.add_argument( + "--price-normal", + choices=("exact", "hermite25"), + default="exact", + ) + parser.add_argument("--spot-count", type=int, default=33) + parser.add_argument("--collocation-stride", type=int, default=1) + parser.add_argument("--boundary-order", type=int, default=4) + parser.add_argument("--price-order", type=int, default=12) + parser.add_argument("--third-predictor-alpha", type=float, default=1.0) + parser.add_argument("--contract-limit", type=int) + parser.add_argument("--contract-seed", type=lambda value: int(value, 0)) + parser.add_argument("--contract-count", type=int) + parser.add_argument("--report", type=pathlib.Path) + args = parser.parse_args() + + contracts = base.contracts(0x51D3, 24) + if args.contract_seed is not None: + if args.contract_count is None: + raise SystemExit("--contract-seed requires --contract-count") + contracts = base.contracts(args.contract_seed, args.contract_count) + if args.contract_limit is not None: + contracts = contracts[:args.contract_limit] + + log_spots = np.linspace(-0.75, 0.75, args.spot_count) + spots = np.exp(log_spots) + qdfp = base.QdFpSurfacePricer() + method_class = ( + SmoothPastingKbi + if args.boundary_equation == "smooth" + else KimBoundaryIntegration + ) + method = method_class( + args.nodes, + args.quadrature_order, + args.grading, + args.product_basis, + args.fh_degree, + args.root_solver, + args.newton_steps, + args.price_mesh, + args.price_power, + args.late_newton_steps, + args.newton_cutover, + args.derivative_mode, + args.price_boundary_interp, + args.boundary_normal, + args.price_normal, + args.collocation_stride, + args.boundary_order, + args.price_order, + args.third_predictor_alpha, + ) + errors: dict[str, list[float]] = {"call": [], "put": []} + signed_errors: dict[str, list[float]] = {"call": [], "put": []} + work_rows: list[dict[str, object]] = [] + contract_rows: list[dict[str, object]] = [] + + for contract in contracts: + maturity = contract.days / 365.0 + put_boundary = method.boundary(maturity, contract.r, contract.q, contract.sigma) + put_values, _, put_price_points = method.put_prices( + spots, maturity, contract.r, contract.q, contract.sigma, put_boundary + ) + # Exact American duality C(S,K;r,q) = P(K,S;q,r). With K=1, + # normalize the dual put by its strike S and use moneyness 1/S. + call_boundary = method.boundary(maturity, contract.q, contract.r, contract.sigma) + dual_put, _, call_price_points = method.put_prices( + 1.0 / spots, + maturity, + contract.q, + contract.r, + contract.sigma, + call_boundary, + ) + call_values = spots * dual_put + truths = { + "call": qdfp.surface(spots, contract, contract.days, True), + "put": qdfp.surface(spots, contract, contract.days, False), + } + approximations = {"call": call_values, "put": put_values} + leg_rows: dict[str, object] = {} + for name in ("call", "put"): + signed = 100.0 * (approximations[name] - truths[name]) + absolute = np.abs(signed) + signed_errors[name].extend(signed.tolist()) + errors[name].extend(absolute.tolist()) + worst = int(np.argmax(absolute)) + leg_rows[name] = { + "max_abs_error_dollars": float(absolute[worst]), + "worst_spot": float(100.0 * spots[worst]), + "signed_error_dollars": float(signed[worst]), + "boundary_at_valuation": float( + call_boundary.values[-1] if name == "call" else put_boundary.values[-1] + ), + } + contract_rows.append({"contract": asdict(contract), **leg_rows}) + for name, boundary, price_points in ( + ("put", put_boundary, put_price_points), + ("call", call_boundary, call_price_points), + ): + work_rows.append({ + "leg": name, + "residual_evaluations": boundary.work.residual_evaluations, + "boundary_kernel_points": boundary.work.boundary_kernel_points, + "price_kernel_points_per_spot": price_points, + "boundary_cdf_evaluations": 2 * boundary.work.boundary_kernel_points, + "price_cdf_evaluations_per_spot": 2 * price_points, + }) + + report = { + "method": "Kim boundary reconstruction plus early-exercise-premium integration", + "runtime_model": "all boundary construction and pricing performed from six live inputs", + "configuration": { + "nodes": args.nodes, + "quadrature_order": args.quadrature_order, + "grading": args.grading, + "boundary_equation": args.boundary_equation, + "product_basis": args.product_basis, + "fh_degree": args.fh_degree, + "root_solver": args.root_solver, + "newton_steps": args.newton_steps, + "price_mesh": args.price_mesh, + "price_power": args.price_power, + "late_newton_steps": args.late_newton_steps, + "newton_cutover": args.newton_cutover, + "derivative_mode": args.derivative_mode, + "price_boundary_interp": args.price_boundary_interp, + "boundary_normal": args.boundary_normal, + "price_normal": args.price_normal, + "spot_count": args.spot_count, + "collocation_stride": args.collocation_stride, + "boundary_order": args.boundary_order, + "price_order": args.price_order, + "third_predictor_alpha": args.third_predictor_alpha, + "contracts": len(contracts), + }, + "errors_dollars_at_100_strike": { + name: metric(values) for name, values in errors.items() + }, + "signed_error_dollars_at_100_strike": { + name: metric(values) for name, values in signed_errors.items() + }, + "work": { + "per_boundary": { + key: metric([float(row[key]) for row in work_rows]) + for key in ( + "residual_evaluations", + "boundary_kernel_points", + "boundary_cdf_evaluations", + "price_kernel_points_per_spot", + "price_cdf_evaluations_per_spot", + ) + }, + "note": "price work is per one requested spot; a transaction prices one leg", + }, + "held_out_contracts": contract_rows, + } + if args.report: + args.report.write_text(json.dumps(report, indent=2) + "\n") + print(json.dumps({ + "configuration": report["configuration"], + "errors_dollars_at_100_strike": report["errors_dollars_at_100_strike"], + "work": report["work"]["per_boundary"], + }, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/scripts/american_quantlib_reference.py b/scripts/american_quantlib_reference.py new file mode 100644 index 0000000..4ac6f28 --- /dev/null +++ b/scripts/american_quantlib_reference.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Shared deterministic QuantLib references for American-option validation. + +This module deliberately contains no ROM/POD machinery. It defines the +normalized contract sample and the QuantLib QdFp reference engine used by KBI +accuracy scripts. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np +import QuantLib as ql + + +EVAL_DATE = ql.Date(15, 6, 2026) +DAY_COUNT = ql.Actual365Fixed() + + +@dataclass(frozen=True) +class Contract: + r: float + q: float + sigma: float + days: int + + +def contracts(seed: int, count: int) -> list[Contract]: + """Return the release's deterministic Latin-hypercube-style sample.""" + rng = np.random.default_rng(seed) + columns: list[np.ndarray] = [] + for _ in range(4): + column = (np.arange(count, dtype=np.float64) + rng.random(count)) / count + rng.shuffle(column) + columns.append(column) + return [ + Contract( + r=0.12 * columns[0][index], + q=0.12 * columns[1][index], + sigma=0.10 + 1.10 * columns[2][index], + days=int(round(30 + 700 * columns[3][index])), + ) + for index in range(count) + ] + + +class QdFpSurfacePricer: + """Normalized American price surfaces from QuantLib 1.41.""" + + def __init__(self) -> None: + ql.Settings.instance().evaluationDate = EVAL_DATE + self.spot = ql.SimpleQuote(1.0) + self.rate = ql.SimpleQuote(0.05) + self.yield_ = ql.SimpleQuote(0.02) + self.vol = ql.SimpleQuote(0.30) + self.process = ql.BlackScholesMertonProcess( + ql.QuoteHandle(self.spot), + ql.YieldTermStructureHandle( + ql.FlatForward(EVAL_DATE, ql.QuoteHandle(self.yield_), DAY_COUNT) + ), + ql.YieldTermStructureHandle( + ql.FlatForward(EVAL_DATE, ql.QuoteHandle(self.rate), DAY_COUNT) + ), + ql.BlackVolTermStructureHandle( + ql.BlackConstantVol( + EVAL_DATE, ql.NullCalendar(), ql.QuoteHandle(self.vol), DAY_COUNT + ) + ), + ) + self.qdfp_engine = ql.QdFpAmericanEngine( + self.process, ql.QdFpAmericanEngine.accurateScheme() + ) + + def surface( + self, + grid: np.ndarray, + contract: Contract, + remaining_days: int, + is_call: bool, + ) -> np.ndarray: + if remaining_days == 0: + if is_call: + return np.maximum(grid - 1.0, 0.0) + return np.maximum(1.0 - grid, 0.0) + self.rate.setValue(contract.r) + self.yield_.setValue(contract.q) + self.vol.setValue(contract.sigma) + option_type = ql.Option.Call if is_call else ql.Option.Put + option = ql.VanillaOption( + ql.PlainVanillaPayoff(option_type, 1.0), + ql.AmericanExercise(EVAL_DATE, EVAL_DATE + remaining_days), + ) + option.setPricingEngine(self.qdfp_engine) + values = np.empty(grid.size, dtype=np.float64) + for index, normalized_spot in enumerate(grid): + self.spot.setValue(float(normalized_spot)) + values[index] = max(option.NPV(), 0.0) + return values diff --git a/scripts/analyze_heston_xi0.py b/scripts/analyze_heston_xi0.py new file mode 100644 index 0000000..ef946ab --- /dev/null +++ b/scripts/analyze_heston_xi0.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Compare emitted xi=0 Heston prices with the exact deterministic reduction.""" + +import math +import sys + +import numpy as np +from scipy.special import ndtr + + +SCALE = 1e12 + + +def main() -> None: + call_errors: list[int] = [] + put_errors: list[int] = [] + worst_call = None + worst_put = None + failures: dict[str, int] = {} + + for line in sys.stdin: + fields = line.strip().split(",") + if fields[7].startswith("E"): + failures[fields[7]] = failures.get(fields[7], 0) + 1 + continue + raw = list(map(int, fields)) + spot_raw, strike_raw, rate_raw, time_raw, v0_raw, kappa_raw, theta_raw, call_raw, put_raw = raw + spot, strike, rate, time, v0, kappa, theta = [ + value / SCALE + for value in (spot_raw, strike_raw, rate_raw, time_raw, v0_raw, kappa_raw, theta_raw) + ] + if kappa == 0.0: + average_variance = v0 + else: + kappa_time = kappa * time + average_variance = theta + (v0 - theta) * (-math.expm1(-kappa_time)) / kappa_time + discounted_strike = strike * math.exp(-rate * time) + if average_variance <= 0.0 or time <= 0.0: + call_reference = max(spot - discounted_strike, 0.0) + put_reference = max(discounted_strike - spot, 0.0) + else: + sigma = math.sqrt(average_variance) + sigma_sqrt_time = sigma * math.sqrt(time) + d1 = ( + math.log(spot / strike) + + rate * time + + 0.5 * average_variance * time + ) / sigma_sqrt_time + d2 = d1 - sigma_sqrt_time + call_reference = spot * ndtr(d1) - discounted_strike * ndtr(d2) + put_reference = call_reference - spot + discounted_strike + call_expected = round(call_reference * SCALE) + put_expected = round(put_reference * SCALE) + call_error = abs(call_raw - call_expected) + put_error = abs(put_raw - put_expected) + call_record = ( + call_error, + spot, + strike, + rate, + time, + v0, + kappa, + theta, + call_raw, + call_expected, + average_variance, + ) + put_record = ( + put_error, + spot, + strike, + rate, + time, + v0, + kappa, + theta, + put_raw, + put_expected, + average_variance, + ) + call_errors.append(call_error) + put_errors.append(put_error) + if worst_call is None or call_error > worst_call[0]: + worst_call = call_record + if worst_put is None or put_error > worst_put[0]: + worst_put = put_record + + for name, values, worst in ( + ("call", call_errors, worst_call), + ("put", put_errors, worst_put), + ): + errors = np.asarray(values, dtype=np.float64) + print( + f"{name} n={len(errors)} median_raw={np.median(errors):.0f} " + f"p95_raw={np.quantile(errors, 0.95, method='lower'):.0f} " + f"p99_raw={np.quantile(errors, 0.99, method='lower'):.0f} " + f"max_raw={errors.max():.0f} worst={worst}" + ) + print(f"failures={failures}") + + +if __name__ == "__main__": + main() diff --git a/scripts/certify_exp_fixed.py b/scripts/certify_exp_fixed.py new file mode 100644 index 0000000..f12bba0 --- /dev/null +++ b/scripts/certify_exp_fixed.py @@ -0,0 +1,1002 @@ +#!/usr/bin/env python3 +"""Source-bound numerical certificate for the N32/Q63 ``exp_fixed_i`` kernel. + +The certificate combines exact integer reasoning with Arb interval arithmetic: + +* the checked-in coefficient source must be byte-identical to the output of + ``generate_exp_coeffs.py``; +* a 120-decimal-digit Remez exchange is checked for equioscillation, while Arb + rigorously bounds the *quantized* polynomial over its whole interval; +* the split-i64 Q63 reduction, all integer widths, all reduction seams, and the + tiny/domain branches are checked against the modeled Rust recurrence; and +* a derivative margin proves monotonicity inside cells, while every actual + cell seam is checked at each raw input in a +/-8 window. + +Reproduce with: + + python3 -m pip install --target /tmp/solmath-proof-deps python-flint==0.8.0 + PYTHONPATH=/tmp/solmath-proof-deps python3 scripts/certify_exp_fixed.py +""" + +from __future__ import annotations + +import argparse +import hashlib +import importlib.util +import json +import math +import re +from pathlib import Path +from typing import Any + +import mpmath as mp + +try: + from flint import arb, ctx +except ImportError as exc: # pragma: no cover - dependency failure + raise SystemExit( + "python-flint==0.8.0 is required; put it on PYTHONPATH" + ) from exc + + +ROOT = Path(__file__).resolve().parents[1] +COEFF_SOURCE = ROOT / "src" / "exp_coeffs.rs" +KERNEL_SOURCE = ROOT / "src" / "transcendental.rs" +CONSTANT_SOURCE = ROOT / "src" / "constants.rs" +EXPM1_SOURCE = ROOT / "src" / "expm1_lut.rs" +GENERATOR_SOURCE = ROOT / "scripts" / "generate_exp_coeffs.py" +CERTIFICATE_SCRIPT = Path(__file__).resolve() +JSON_OUTPUT = ROOT / ".superstack" / "exp-proof-certificate-2026-07-12.json" +MARKDOWN_OUTPUT = ROOT / ".superstack" / "exp-proof-certificate-2026-07-12.md" +PRODUCTION_VECTORS = ROOT / "benchmark" / "prod_exp_vectors.json" +ADVERSARIAL_VECTORS = ROOT / "benchmark" / "adv_exp_vectors.json" + +SCALE = 10**12 +LIMIT = 40 * SCALE +Q63 = 1 << 63 +Q64 = 1 << 64 +Q96 = 1 << 96 +COEFF_GUARD = SCALE << 22 +PHASE_Q = 1 << 62 +ARB_BITS = 256 +ERROR_GRID = 100_000 +SEAM_RADIUS_RAW = 8 +DATE = "2026-07-12" + +MODELED_FUNCTIONS = ( + "round_shift_signed", + "round_shift_i64", + "mul_q63_i64", + "exp_fixed_i", +) + +# Filled after the modeled functions and generated coefficient source were +# frozen. These fail closed if the Rust recurrence or constants drift. +EXPECTED_COEFFICIENT_SHA256 = "a5adbf73f726a1d347c03acf7d732aff2d48fec93499ba308bcc5d9af13c97ac" +EXPECTED_MODELED_KERNEL_SHA256 = "04b8f5eb1e543e2cff99dc936d19ba179cefbe4c74ca1bb2af5ba94ad6a481ed" + + +def sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def extract_rust_function(source: str, name: str) -> str: + match = re.search( + rf"(?m)^(?:pub\(crate\) |pub )?(?:#\[[^\n]+\]\n)*fn {name}" + rf"(?:<[^\n]+>)?\s*\(", + source, + ) + assert match, f"cannot find modeled Rust function {name}" + brace = source.find("{", match.end()) + assert brace >= 0 + depth = 0 + for index in range(brace, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[match.start() : index + 1] + raise AssertionError(f"unterminated Rust function {name}") + + +def parse_scalar(source: str, name: str) -> int: + match = re.search( + rf"\bconst {name}\s*:[^=]+?=\s*(-?[0-9][0-9_]*)(?:[iu][0-9]+)?\s*;", + source, + ) + assert match, f"cannot parse {name}" + return int(match.group(1).replace("_", "")) + + +def parse_array(source: str, name: str) -> tuple[int, ...]: + match = re.search( + rf"\bconst {name}\s*:\s*\[[^\]]+\]\s*=\s*\[(.*?)\];", source, re.S + ) + assert match, f"cannot parse {name}" + return tuple( + int(token.replace("_", "")) + for token in re.findall(r"-?[0-9][0-9_]*", match.group(1)) + ) + + +def import_generator(): + spec = importlib.util.spec_from_file_location("generate_exp_coeffs", GENERATOR_SOURCE) + assert spec and spec.loader + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def source_binding() -> tuple[dict[str, Any], Any, dict[str, int], tuple[int, ...], tuple[int, ...]]: + coefficient_text = COEFF_SOURCE.read_text() + kernel_text = KERNEL_SOURCE.read_text() + constant_text = CONSTANT_SOURCE.read_text() + expm1_text = EXPM1_SOURCE.read_text() + generator = import_generator() + assert generator.render() == coefficient_text, "exp_coeffs.rs is not generator-reproducible" + + modeled_text = "\n\n".join( + extract_rust_function(kernel_text, name) for name in MODELED_FUNCTIONS + ) + modeled_sha = hashlib.sha256(modeled_text.encode()).hexdigest() + coefficient_sha = hashlib.sha256(coefficient_text.encode()).hexdigest() + if EXPECTED_COEFFICIENT_SHA256 != "TO_FILL": + assert coefficient_sha == EXPECTED_COEFFICIENT_SHA256 + if EXPECTED_MODELED_KERNEL_SHA256 != "TO_FILL": + assert modeled_sha == EXPECTED_MODELED_KERNEL_SHA256 + + constants = { + "SCALE_I": parse_scalar(constant_text, "SCALE_I"), + "LN2_I": parse_scalar(constant_text, "LN2_I"), + "EXPM1_INV_LN2_Q56": parse_scalar(expm1_text, "EXPM1_INV_LN2_Q56"), + "EXP_RAW_TO_Q63_HI": parse_scalar(coefficient_text, "EXP_RAW_TO_Q63_HI"), + "EXP_RAW_TO_Q63_FRAC_Q28": parse_scalar( + coefficient_text, "EXP_RAW_TO_Q63_FRAC_Q28" + ), + "EXP_LN2_RESIDUAL_Q96": parse_scalar( + coefficient_text, "EXP_LN2_RESIDUAL_Q96" + ), + "EXP_STEP_Q63": parse_scalar(coefficient_text, "EXP_STEP_Q63"), + "EXP_POLY_GUARD": parse_scalar(coefficient_text, "EXP_POLY_GUARD"), + "EXP_PHASE_BITS": parse_scalar(coefficient_text, "EXP_PHASE_BITS"), + # Retained only to reproduce the pre-optimization rational kernel in + # the before/after corpus comparison; they are not used by the proof. + "LN2_LO": parse_scalar(constant_text, "LN2_LO"), + "EXP_REMEZ_P1": parse_scalar(constant_text, "EXP_REMEZ_P1"), + "EXP_REMEZ_P2": parse_scalar(constant_text, "EXP_REMEZ_P2"), + "EXP_REMEZ_P3": parse_scalar(constant_text, "EXP_REMEZ_P3"), + "EXP_REMEZ_P4": parse_scalar(constant_text, "EXP_REMEZ_P4"), + "EXP_REMEZ_P5": parse_scalar(constant_text, "EXP_REMEZ_P5"), + } + coefficients = parse_array(coefficient_text, "EXP_REMEZ_Q22") + phases = parse_array(coefficient_text, "EXP2_PHASE_Q62") + + assert constants == { + "SCALE_I": SCALE, + "LN2_I": 693_147_180_560, + "EXPM1_INV_LN2_Q56": 103_957, + "EXP_RAW_TO_Q63_HI": 18_446_744, + "EXP_RAW_TO_Q63_FRAC_Q28": 19_786_257, + "EXP_LN2_RESIDUAL_Q96": -4_333_034_379_533_306, + "EXP_STEP_Q63": 199_786_072_581_291_495, + "EXP_POLY_GUARD": 22, + "EXP_PHASE_BITS": 5, + "LN2_LO": -54_690_582_768, + "EXP_REMEZ_P1": 166_666_666_667, + "EXP_REMEZ_P2": -2_777_777_778, + "EXP_REMEZ_P3": 66_137_563, + "EXP_REMEZ_P4": -1_653_390, + "EXP_REMEZ_P5": 41_381, + } + assert len(coefficients) == 6 + assert len(phases) == 32 + assert all(0 < value <= (1 << 63) - 1 for value in coefficients) + assert all(0 < value <= (1 << 63) - 1 for value in phases) + assert "(-1_000_000..1_000_000).contains(&x)" in modeled_text + assert "round_shift_i64(scaled_residual, 1)" in modeled_text + assert "(56 - EXP_PHASE_BITS)" in modeled_text + assert "EXP_POLY_GUARD + 62" in modeled_text + + binding = { + "coefficient_source_sha256": coefficient_sha, + "modeled_kernel_sha256": modeled_sha, + "modeled_functions": list(MODELED_FUNCTIONS), + "full_source_sha256": { + "src/exp_coeffs.rs": sha256(COEFF_SOURCE), + "src/transcendental.rs": sha256(KERNEL_SOURCE), + "src/constants.rs": sha256(CONSTANT_SOURCE), + "src/expm1_lut.rs": sha256(EXPM1_SOURCE), + "scripts/generate_exp_coeffs.py": sha256(GENERATOR_SOURCE), + "scripts/certify_exp_fixed.py": sha256(CERTIFICATE_SCRIPT), + }, + "generator_byte_identical": True, + } + return binding, generator, constants, coefficients, phases + + +def round_shift(value: int, shift: int) -> int: + assert shift > 0 + half = 1 << (shift - 1) + if value >= 0: + return (value + half) >> shift + return -((-value + half) >> shift) + + +def reduce_model(x: int, constants: dict[str, int]) -> dict[str, int]: + x64 = x + octave = round_shift(x64 * constants["EXPM1_INV_LN2_Q56"], 56) + raw = x64 - octave * constants["LN2_I"] + scaled = raw * constants["EXP_RAW_TO_Q63_HI"] + round_shift( + raw * constants["EXP_RAW_TO_Q63_FRAC_Q28"], 28 + ) + octave_q63 = round_shift(scaled, 1) - round_shift( + octave * constants["EXP_LN2_RESIDUAL_Q96"], 33 + ) + subcell = round_shift( + raw * constants["EXPM1_INV_LN2_Q56"], + 56 - constants["EXP_PHASE_BITS"], + ) + r_q63 = octave_q63 - subcell * constants["EXP_STEP_Q63"] + half_step = (constants["EXP_STEP_Q63"] + 1) // 2 + if r_q63 > half_step: + subcell += 1 + r_q63 -= constants["EXP_STEP_Q63"] + elif r_q63 < -half_step: + subcell -= 1 + r_q63 += constants["EXP_STEP_Q63"] + phases = 1 << constants["EXP_PHASE_BITS"] + cell = octave * phases + subcell + return { + "octave_estimate": octave, + "raw_residual": raw, + "scaled_residual": scaled, + "subcell": subcell, + "r_q63": r_q63, + "cell": cell, + "octave": cell >> constants["EXP_PHASE_BITS"], + "phase": cell & (phases - 1), + } + + +def exp_model( + x: int, + constants: dict[str, int], + coefficients: tuple[int, ...], + phases: tuple[int, ...], +) -> tuple[str, int | str]: + if x <= -LIMIT: + return "ok", 0 + if x >= LIMIT: + return "error", "Overflow" + if -1_000_000 <= x < 1_000_000: + return "ok", SCALE + x + + reduction = reduce_model(x, constants) + r_q63 = reduction["r_q63"] + polynomial = coefficients[0] + for coefficient in coefficients[1:]: + polynomial = round_shift(polynomial * r_q63, 63) + coefficient + + phase = reduction["phase"] + if phase == 0: + guarded = polynomial + guard = constants["EXP_POLY_GUARD"] + else: + guarded = polynomial * phases[phase] + guard = constants["EXP_POLY_GUARD"] + 62 + shift = guard - reduction["octave"] + if shift >= 128: + return "ok", 0 + if shift > 0: + return "ok", round_shift(guarded, shift) + if shift == 0: + return "ok", guarded + result = guarded << -shift + assert result <= (1 << 127) - 1 + return "ok", result + + +def trunc_div(numerator: int, denominator: int) -> int: + """Rust-style signed integer division (toward zero).""" + assert denominator != 0 + negative = (numerator < 0) != (denominator < 0) + quotient = abs(numerator) // abs(denominator) + return -quotient if negative else quotient + + +def legacy_exp_model(x: int, constants: dict[str, int]) -> tuple[str, int | str]: + """Exact simulator for the replaced SCALE/Remez rational recurrence.""" + if x <= -LIMIT: + return "ok", 0 + if x >= LIMIT: + return "error", "Overflow" + if x == 0: + return "ok", SCALE + + octave = trunc_div(x, constants["LN2_I"]) + raw_correction = octave * constants["LN2_LO"] + if raw_correction >= 0: + ln2_correction = trunc_div(raw_correction + SCALE // 2, SCALE) + else: + ln2_correction = trunc_div(raw_correction - SCALE // 2, SCALE) + residual = x - octave * constants["LN2_I"] - ln2_correction + half_ln2 = constants["LN2_I"] // 2 + if residual > half_ln2: + octave += 1 + residual -= constants["LN2_I"] + elif residual < -half_ln2: + octave -= 1 + residual += constants["LN2_I"] + + def mul_round(left: int, right: int) -> int: + product = left * right + quotient = trunc_div(product, SCALE) + remainder = product - quotient * SCALE + if abs(remainder) < SCALE // 2: + return quotient + return quotient + (1 if product >= 0 else -1) + + xx = mul_round(residual, residual) + polynomial = mul_round(xx, constants["EXP_REMEZ_P5"]) + constants["EXP_REMEZ_P4"] + polynomial = mul_round(xx, polynomial) + constants["EXP_REMEZ_P3"] + polynomial = mul_round(xx, polynomial) + constants["EXP_REMEZ_P2"] + polynomial = mul_round(xx, polynomial) + constants["EXP_REMEZ_P1"] + correction = residual - mul_round(polynomial, xx) + rc = mul_round(residual, correction) + rational = trunc_div(rc * SCALE, 2 * SCALE - correction) + reduced = SCALE + residual + rational + if octave >= 0: + return "ok", reduced << octave + return "ok", reduced >> -octave + + +def arb_text(value: arb, digits: int = 40) -> str: + return value.str(digits, radius=False) + + +def upper_abs(value: arb) -> arb: + return abs(value).abs_upper() + + +def certify_polynomial(generator, coefficients: tuple[int, ...]) -> dict[str, Any]: + ctx.prec = ARB_BITS + mp.mp.dps = generator.WORK_DPS + real_coefficients, alternation_error, extrema = generator.derive_remez() + quantized_ascending = tuple(reversed(coefficients)) + regenerated = tuple( + generator.round_away(value * COEFF_GUARD) for value in real_coefficients + ) + assert regenerated == quantized_ascending + + alternation_values = [ + sum(value * x**power for power, value in enumerate(real_coefficients)) + - mp.exp(x) + for x in extrema + ] + assert all(left * right < 0 for left, right in zip(alternation_values, alternation_values[1:])) + alternation_spread = max(abs(value) for value in alternation_values) - min( + abs(value) for value in alternation_values + ) + assert alternation_spread < mp.mpf("1e-100") + + radius = arb(2).log() / 64 + degree = len(quantized_ascending) - 1 + derivative_error_bound = arb(0) + radius_power = arb(1) + for power in range(degree): + delta = ( + arb((power + 1) * quantized_ascending[power + 1]) / COEFF_GUARD + - arb(1) / math.factorial(power) + ) + derivative_error_bound += upper_abs(delta) * radius_power + radius_power *= radius + derivative_error_bound += radius**degree * radius.exp().abs_upper() / math.factorial(degree) + + maximum_node_error = arb(0) + maximum_index = 0 + for index in range(ERROR_GRID + 1): + x = -radius + 2 * radius * index / ERROR_GRID + polynomial = arb(quantized_ascending[-1]) / COEFF_GUARD + for coefficient in reversed(quantized_ascending[:-1]): + polynomial = polynomial * x + arb(coefficient) / COEFF_GUARD + error = upper_abs(polynomial - x.exp()) + if error > maximum_node_error: + maximum_node_error = error + maximum_index = index + continuous_bound = maximum_node_error + derivative_error_bound * radius / ERROR_GRID + assert continuous_bound < arb("7.05e-17") + return { + "degree": degree, + "interval": "[-ln(2)/64, ln(2)/64]", + "remez_alternation_error": mp.nstr(alternation_error, 80), + "remez_extrema": [mp.nstr(value, 40) for value in extrema], + "alternation_spread": mp.nstr(alternation_spread, 8), + "quantized_continuous_bound": arb_text(continuous_bound, 55), + "maximum_grid_node_bound": arb_text(maximum_node_error, 55), + "maximum_grid_node_index": maximum_index, + "derivative_error_bound": arb_text(derivative_error_bound, 55), + "arb_bits": ARB_BITS, + "mvt_grid_intervals": ERROR_GRID, + "coefficients_descending_q22": list(coefficients), + "_continuous": continuous_bound, + "_derivative_error": derivative_error_bound, + } + + +def ceil_div(numerator: int, denominator: int) -> int: + return -((-numerator) // denominator) + + +def exact_raw_residual_bound(constants: dict[str, int]) -> tuple[int, dict[str, int]]: + reciprocal = constants["EXPM1_INV_LN2_Q56"] + denominator = 1 << 56 + half = 1 << 55 + maximum = 0 + witness: dict[str, int] = {} + for octave in range(0, 100): + lo = max(0, ceil_div(octave * denominator - half, reciprocal)) + hi = min(LIMIT - 1, ((octave + 1) * denominator - half - 1) // reciprocal) + if lo > hi: + continue + for x in (lo, hi): + residual = x - octave * constants["LN2_I"] + if abs(residual) > maximum: + maximum = abs(residual) + witness = {"x": x, "octave": octave, "raw_residual": residual} + assert maximum == 346_624_802_184 + return maximum, witness + + +def reduction_and_width_certificate( + constants: dict[str, int], coefficients: tuple[int, ...], phases: tuple[int, ...] +) -> dict[str, Any]: + ctx.prec = ARB_BITS + ln2 = arb(2).log() + phase_count = 1 << constants["EXP_PHASE_BITS"] + reciprocal = arb(constants["EXPM1_INV_LN2_Q56"]) / (1 << 56) + exact_reciprocal = arb(1) / (SCALE * ln2) + candidate_cell_error = ( + arb(1) / 2 + + phase_count * LIMIT * upper_abs(reciprocal - exact_reciprocal) + + phase_count + * 58 + * upper_abs(arb(1) - constants["LN2_I"] * reciprocal) + ) + assert candidate_cell_error < arb("0.51") + + max_raw, raw_witness = exact_raw_residual_bound(constants) + exact_raw_multiplier = arb(2) ** 64 / SCALE + split_raw_multiplier = arb(constants["EXP_RAW_TO_Q63_HI"]) + arb( + constants["EXP_RAW_TO_Q63_FRAC_Q28"] + ) / (1 << 28) + split_q64_error = arb(1) / 2 + max_raw * upper_abs( + split_raw_multiplier - exact_raw_multiplier + ) + q63_conversion_error = split_q64_error / 2 + arb(1) / 2 + + exact_ln2_residual_q96 = (ln2 - arb(constants["LN2_I"]) / SCALE) * Q96 + ln2_residual_error = arb(1) / 2 + 58 * upper_abs( + arb(constants["EXP_LN2_RESIDUAL_Q96"]) - exact_ln2_residual_q96 + ) / (1 << 33) + step_error_per_subcell = upper_abs( + arb(constants["EXP_STEP_Q63"]) - ln2 / phase_count * Q63 + ) + # The raw proposal is in [-16,16]; allowing one correction gives 17. + subcell_bound = 17 + step_error = subcell_bound * step_error_per_subcell + reduction_error_q63 = q63_conversion_error + ln2_residual_error + step_error + assert reduction_error_q63 < arb(72) + reduction_error_real = reduction_error_q63 / Q63 + ambiguous_raw_radius = reduction_error_real * SCALE + assert ambiguous_raw_radius < arb("0.00001") + + i64_max = (1 << 63) - 1 + i128_max = (1 << 127) - 1 + raw_hi_product = max_raw * constants["EXP_RAW_TO_Q63_HI"] + raw_frac_product = max_raw * constants["EXP_RAW_TO_Q63_FRAC_Q28"] + scaled_residual_bound = raw_hi_product + round_shift(raw_frac_product, 28) + octave_residual_product = 58 * abs(constants["EXP_LN2_RESIDUAL_Q96"]) + subcell_proposal_product = max_raw * constants["EXPM1_INV_LN2_Q56"] + r_bound = (constants["EXP_STEP_Q63"] + 1) // 2 + 1 + + assert max(raw_hi_product, raw_frac_product, scaled_residual_bound) <= i64_max + assert octave_residual_product <= i64_max + assert subcell_proposal_product <= i64_max + assert r_bound <= i64_max + + accumulator_bound = abs(coefficients[0]) + maximum_horner_product = 0 + accumulator_bounds = [accumulator_bound] + for coefficient in coefficients[1:]: + product_bound = accumulator_bound * r_bound + maximum_horner_product = max(maximum_horner_product, product_bound) + accumulator_bound = (product_bound + (1 << 62)) // Q63 + abs(coefficient) + accumulator_bounds.append(accumulator_bound) + assert accumulator_bound <= i64_max + assert maximum_horner_product <= i128_max + maximum_phase = max(phases) + maximum_phase_product = accumulator_bound * maximum_phase + assert maximum_phase <= i64_max + assert maximum_phase_product <= i128_max + + max_input = LIMIT - 1 + status, maximum_output = exp_model(max_input, constants, coefficients, phases) + assert status == "ok" and isinstance(maximum_output, int) + assert maximum_output < i128_max + + return { + "candidate_cell_error_bound": arb_text(candidate_cell_error, 55), + "candidate_within_one_correction": True, + "maximum_exact_raw_octave_residual": max_raw, + "raw_residual_witness": raw_witness, + "split_q64_error_bound": arb_text(split_q64_error, 55), + "q63_conversion_error_bound": arb_text(q63_conversion_error, 55), + "ln2_residual_error_bound_q63": arb_text(ln2_residual_error, 55), + "step_error_bound_q63": arb_text(step_error, 55), + "total_reduction_error_bound_q63": arb_text(reduction_error_q63, 55), + "total_reduction_error_bound_real": arb_text(reduction_error_real, 55), + "ambiguous_raw_radius": arb_text(ambiguous_raw_radius, 30), + "subcell_bound_including_correction": subcell_bound, + "widths": { + "i64_max": i64_max, + "i128_max": i128_max, + "raw_times_hi": raw_hi_product, + "raw_times_frac_q28": raw_frac_product, + "scaled_residual": scaled_residual_bound, + "octave_times_ln2_residual_q96": octave_residual_product, + "raw_times_inv_ln2_q56": subcell_proposal_product, + "maximum_abs_r_q63": r_bound, + "horner_accumulator_bounds": accumulator_bounds, + "maximum_horner_product": maximum_horner_product, + "maximum_phase_factor_q62": maximum_phase, + "maximum_phase_product": maximum_phase_product, + "maximum_output_at_40_minus_one": maximum_output, + "horner_product_fraction_i128": maximum_horner_product / i128_max, + "phase_product_fraction_i128": maximum_phase_product / i128_max, + }, + "_reduction_real": reduction_error_real, + } + + +def exact_cell(x: int) -> int: + return int(mp.floor(mp.mpf(x) * 32 / (SCALE * mp.log(2)) + mp.mpf("0.5"))) + + +def seam_and_monotonicity_certificate( + constants: dict[str, int], + coefficients: tuple[int, ...], + phases: tuple[int, ...], + reduction: dict[str, Any], + polynomial: dict[str, Any], +) -> dict[str, Any]: + mp.mp.dps = 100 + seams = [] + checked_inputs = 0 + monotone_pairs = 0 + maximum_jump = 0 + minimum_boundary_distance = None + for boundary_cell in range(-2000, 2000): + boundary = (mp.mpf(boundary_cell) + mp.mpf("0.5")) * mp.log(2) / 32 * SCALE + center = int(mp.floor(boundary + mp.mpf("0.5"))) + if center - SEAM_RADIUS_RAW <= -LIMIT or center + SEAM_RADIUS_RAW >= LIMIT: + continue + boundary_ball = (arb(boundary_cell) + arb(1) / 2) * arb(2).log() / 32 * SCALE + boundary_distance = abs(boundary_ball - center).lower() + if minimum_boundary_distance is None or boundary_distance < minimum_boundary_distance: + minimum_boundary_distance = boundary_distance + seams.append(boundary_cell) + xs = list(range(center - SEAM_RADIUS_RAW, center + SEAM_RADIUS_RAW + 1)) + outputs: list[int] = [] + for x in xs: + reduction_result = reduce_model(x, constants) + if arb(x) < boundary_ball: + interval_exact_cell = boundary_cell + elif arb(x) > boundary_ball: + interval_exact_cell = boundary_cell + 1 + else: # pragma: no cover - 256-bit Arb separates every checked raw x + raise AssertionError("seam boundary overlaps an integer raw input") + assert interval_exact_cell == exact_cell(x) + assert reduction_result["cell"] == interval_exact_cell + status, output = exp_model(x, constants, coefficients, phases) + assert status == "ok" and isinstance(output, int) + outputs.append(output) + checked_inputs += len(xs) + for left, right in zip(outputs, outputs[1:]): + assert right >= left + maximum_jump = max(maximum_jump, right - left) + monotone_pairs += 1 + assert len(seams) == 3_694 + assert minimum_boundary_distance is not None + # Outside each +/-8 window the nearest possible raw input is separated + # from its seam by more than seven units, versus <1e-5 raw reduction + # uncertainty. Inside the windows the Arb comparisons above are exact. + assert arb(reduction["ambiguous_raw_radius"]) < arb(7) + + # The cheap full-octave proposal changes at points near half-ln2 cell + # centers, not at N32 cell boundaries. Its two algebraic decompositions + # represent the same final cell, but rounded split conversion can differ + # by a few Q63 units, so check every such internal transition explicitly. + proposal_transitions: set[int] = set() + proposal_denominator = 1 << 56 + proposal_half = 1 << 55 + proposal_reciprocal = constants["EXPM1_INV_LN2_Q56"] + for new_octave in range(1, 59): + positive = ceil_div( + new_octave * proposal_denominator - proposal_half, + proposal_reciprocal, + ) + proposal_transitions.add(positive) + proposal_transitions.add(-positive) + assert len(proposal_transitions) == 116 + proposal_transition_inputs = 0 + proposal_transition_pairs = 0 + for center in sorted(proposal_transitions): + xs = list(range(center - SEAM_RADIUS_RAW, center + SEAM_RADIUS_RAW + 1)) + outputs = [] + for x in xs: + assert -LIMIT < x < LIMIT + assert reduce_model(x, constants)["cell"] == exact_cell(x) + status, output = exp_model(x, constants, coefficients, phases) + assert status == "ok" and isinstance(output, int) + outputs.append(output) + proposal_transition_inputs += len(xs) + for left, right in zip(outputs, outputs[1:]): + assert right >= left + proposal_transition_pairs += 1 + + # Inside one cell the exact quantized polynomial rises by millions of + # guarded units per raw input. This dominates both Horner evaluations' + # complete rounding envelopes, so the integer polynomial cannot reverse. + ctx.prec = ARB_BITS + radius = arb(2).log() / 64 + reduction["_reduction_real"] + ascending = tuple(reversed(coefficients)) + derivative_lower = ( + arb(ascending[1]) / COEFF_GUARD + - 2 * arb(ascending[2]) / COEFF_GUARD * radius + - 4 * arb(ascending[4]) / COEFF_GUARD * radius**3 + ) + assert derivative_lower > arb("0.98") + minimum_q63_step = constants["EXP_RAW_TO_Q63_HI"] // 2 + horner_rounding_guarded = arb(1) / 2 * sum( + radius**power for power in range(len(coefficients) - 1) + ) + guarded_step_margin = ( + derivative_lower * minimum_q63_step / Q63 * COEFF_GUARD + - 2 * horner_rounding_guarded + ) + assert guarded_step_margin > arb(1_000_000) + + # Tiny direct-return seams and the complete domain contract. + tiny_xs = list(range(-1_000_008, -999_991)) + list(range(999_992, 1_000_009)) + tiny_outputs = [] + for x in tiny_xs: + status, output = exp_model(x, constants, coefficients, phases) + assert status == "ok" and isinstance(output, int) + expected = int(mp.floor(mp.exp(mp.mpf(x) / SCALE) * SCALE + mp.mpf("0.5"))) + assert abs(output - expected) <= 1 + tiny_outputs.append(output) + assert all(right >= left for left, right in zip(tiny_outputs, tiny_outputs[1:])) + + tiny_radius = arb(999_999) / SCALE + tiny_remainder = SCALE * tiny_radius**2 * tiny_radius.exp() / 2 + assert tiny_remainder < arb("0.5") + + domain_cases = {} + for x in (-LIMIT - 1, -LIMIT, -LIMIT + 1, LIMIT - 1, LIMIT, LIMIT + 1): + status, value = exp_model(x, constants, coefficients, phases) + domain_cases[str(x)] = {"status": status, "value": value} + assert domain_cases[str(-LIMIT - 1)] == {"status": "ok", "value": 0} + assert domain_cases[str(-LIMIT)] == {"status": "ok", "value": 0} + assert domain_cases[str(LIMIT)] == {"status": "error", "value": "Overflow"} + assert domain_cases[str(LIMIT + 1)] == {"status": "error", "value": "Overflow"} + + return { + "actual_cell_seams": len(seams), + "raw_radius_per_seam": SEAM_RADIUS_RAW, + "seam_input_checks": checked_inputs, + "seam_monotone_pairs": monotone_pairs, + "cell_mismatches": 0, + "seam_reversals": 0, + "maximum_observed_adjacent_jump": maximum_jump, + "minimum_seam_to_integer_distance_raw": str(minimum_boundary_distance), + "outside_window_distance_lower_raw": 7, + "octave_proposal_internal_seams": len(proposal_transitions), + "octave_proposal_input_checks": proposal_transition_inputs, + "octave_proposal_monotone_pairs": proposal_transition_pairs, + "octave_proposal_reversals": 0, + "interior_derivative_lower": arb_text(derivative_lower, 45), + "minimum_q63_increment_per_raw_input": minimum_q63_step, + "horner_rounding_envelope_guarded": arb_text(horner_rounding_guarded, 45), + "interior_guarded_step_margin": arb_text(guarded_step_margin, 45), + "interior_monotonicity_proved": True, + "tiny_taylor_remainder_bound_raw": arb_text(tiny_remainder, 45), + "tiny_seam_checks": len(tiny_xs), + "domain_cases": domain_cases, + } + + +def final_error_certificate( + constants: dict[str, int], + phases: tuple[int, ...], + polynomial: dict[str, Any], + reduction: dict[str, Any], +) -> dict[str, Any]: + ctx.prec = ARB_BITS + radius = arb(2).log() / 64 + extended_radius = radius + reduction["_reduction_real"] + continuous = polynomial["_continuous"] + polynomial["_derivative_error"] * reduction[ + "_reduction_real" + ] + input_exp_error = extended_radius.exp() * reduction["_reduction_real"] + degree = len(polynomial["coefficients_descending_q22"]) - 1 + horner_rounding = arb(1) / (2 * COEFF_GUARD) * sum( + extended_radius**power for power in range(degree) + ) + phase_max = arb(2) ** (arb(31) / 32) + phase_quantization = arb(1) / (2 * PHASE_Q) + polynomial_magnitude = extended_radius.exp() + continuous + input_exp_error + horner_rounding + local_error = phase_max * (continuous + input_exp_error + horner_rounding) + ( + polynomial_magnitude * phase_quantization + ) + relative_error = local_error / (-radius).exp() + + full_raw = local_error * SCALE * (1 << 57) + arb(1) / 2 + financial_raw = local_error * SCALE * (1 << 28) + arb(1) / 2 + assert relative_error < arb("2e-16") + assert financial_raw < arb(50_000) + assert full_raw < arb("3e13") + + return { + "extended_residual_radius": arb_text(extended_radius, 55), + "continuous_bound_on_extended_radius": arb_text(continuous, 55), + "input_reduction_exp_contribution": arb_text(input_exp_error, 55), + "integer_horner_rounding_contribution": arb_text(horner_rounding, 55), + "phase_quantization_contribution": arb_text( + polynomial_magnitude * phase_quantization, 55 + ), + "combined_local_absolute_bound": arb_text(local_error, 55), + "combined_relative_bound": arb_text(relative_error, 55), + "financial_domain": "|x| < 20*SCALE, maximum reconstruction octave 28", + "financial_raw_ulp_bound": arb_text(financial_raw, 45), + "full_domain": "-40*SCALE < x < 40*SCALE, maximum reconstruction octave 57", + "full_raw_ulp_bound": arb_text(full_raw, 45), + } + + +def percentile(sorted_values: list[int], fraction: float) -> int: + return sorted_values[int(fraction * (len(sorted_values) - 1))] + + +def retained_corpus_stats( + path: Path, + constants: dict[str, int], + coefficients: tuple[int, ...], + phases: tuple[int, ...], + legacy: bool = False, +) -> dict[str, Any]: + if not path.exists(): + return {"present": False} + payload = json.loads(path.read_text()) + errors: list[int] = [] + worst: dict[str, Any] | None = None + for vector in payload["vectors"]: + x = int(vector["x"]) + status, output = ( + legacy_exp_model(x, constants) + if legacy + else exp_model(x, constants, coefficients, phases) + ) + assert status == "ok" and isinstance(output, int) + expected = int(vector["expected"]) + error = abs(output - expected) + errors.append(error) + if worst is None or error > worst["error"]: + worst = { + "error": error, + "x": x, + "category": vector.get("category"), + "output": output, + "expected": expected, + } + errors.sort() + count = len(errors) + return { + "present": True, + "path": str(path.relative_to(ROOT)), + "sha256": sha256(path), + "count": count, + "max": errors[-1], + "p99": percentile(errors, 0.99), + "p95": percentile(errors, 0.95), + "median": errors[count // 2], + "exact": sum(error == 0 for error in errors), + "worst": worst, + "meta": payload.get("meta", {}), + "kernel": "legacy SCALE/Remez rational simulator" if legacy else "frozen N32/Q63", + } + + +def strip_private(data: Any) -> Any: + if isinstance(data, dict): + return {key: strip_private(value) for key, value in data.items() if not key.startswith("_")} + if isinstance(data, list): + return [strip_private(value) for value in data] + return data + + +def render_markdown(certificate: dict[str, Any]) -> str: + source = certificate["source_binding"] + poly = certificate["polynomial"] + reduction = certificate["reduction"] + monotone = certificate["monotonicity"] + errors = certificate["error_bounds"] + prod = certificate["retained_corpora"]["production"] + adv = certificate["retained_corpora"]["adversarial"] + legacy_prod = certificate["legacy_comparison"]["production"] + legacy_adv = certificate["legacy_comparison"]["adversarial"] + widths = reduction["widths"] + return f"""# `exp_fixed_i` proof certificate ({DATE}) + +## Result + +The exact N32/Q63 Rust recurrence bound by this certificate is division-free, +uses a degree-5 quantized Remez polynomial, and is monotone on every valid raw +input. Arb proves the quantized local polynomial error is at most +`{poly['quantized_continuous_bound']}`. Including split range reduction, +integer Horner rounding and Q62 phase reconstruction gives: + +| Bound | Certified value | +|---|---:| +| relative error over `(-40,40)` | `{errors['combined_relative_bound']}` | +| raw error for `|x| < 20*SCALE` | `{errors['financial_raw_ulp_bound']}` | +| raw error for the full valid domain | `{errors['full_raw_ulp_bound']}` | + +## Source binding + +- `src/exp_coeffs.rs`: `{source['coefficient_source_sha256']}` +- modeled Rust functions: `{source['modeled_kernel_sha256']}` +- functions: {', '.join(source['modeled_functions'])} +- generator output is byte-identical to the checked-in coefficient source. + +## Approximation + +- high-precision Remez alternation error: `{poly['remez_alternation_error']}` +- exact quantized coefficient bound: `{poly['quantized_continuous_bound']}` +- Arb precision/grid: {poly['arb_bits']} bits / {poly['mvt_grid_intervals']:,} intervals +- coefficients, descending Q22: `{poly['coefficients_descending_q22']}` + +The Remez exchange establishes the origin and equioscillation of the real +coefficients. The stated error theorem does not trust sampled Remez error: it +uses Arb balls on the exact quantized coefficients plus a mean-value enclosure. + +## Reduction and monotonicity + +- candidate-cell error: `{reduction['candidate_cell_error_bound']}` cell units +- complete Q63 reduction error: `{reduction['total_reduction_error_bound_q63']}` units +- equivalent ambiguous raw radius: `{reduction['ambiguous_raw_radius']}` +- actual cell seams: {monotone['actual_cell_seams']:,} +- raw checks: {monotone['seam_input_checks']:,} (`+/-{monotone['raw_radius_per_seam']}` each) +- cell mismatches / output reversals: {monotone['cell_mismatches']} / {monotone['seam_reversals']} +- internal octave-proposal seams: {monotone['octave_proposal_internal_seams']:,} + ({monotone['octave_proposal_input_checks']:,} raw checks, {monotone['octave_proposal_reversals']} reversals) +- within-cell guarded step margin: `{monotone['interior_guarded_step_margin']}` +- tiny direct-return remainder: `{monotone['tiny_taylor_remainder_bound_raw']}` raw units + +N32 has 3,694 actual reduction seams. The earlier 7,386-seam count applied to +the rejected N64 candidate; this certificate checks every seam of the frozen +N32 source, with 62,798 raw evaluations. + +## Integer widths + +| Intermediate | Maximum/bound | +|---|---:| +| raw residual | {reduction['maximum_exact_raw_octave_residual']:,} | +| split scaled residual | {widths['scaled_residual']:,} | +| Q63 residual | {widths['maximum_abs_r_q63']:,} | +| Horner product | {widths['maximum_horner_product']:,} | +| phase product | {widths['maximum_phase_product']:,} | +| phase product / `i128::MAX` | {widths['phase_product_fraction_i128']:.6f} | +| output at `40*SCALE-1` | {widths['maximum_output_at_40_minus_one']:,} | + +## Retained corpora + +| Corpus | N | Max | P99 | P95 | Median | Exact | +|---|---:|---:|---:|---:|---:|---:| +| production | {prod.get('count', 0):,} | {prod.get('max', 0):,} | {prod.get('p99', 0):,} | {prod.get('p95', 0):,} | {prod.get('median', 0):,} | {prod.get('exact', 0):,} | +| structural adversarial | {adv.get('count', 0):,} | {adv.get('max', 0):,} | {adv.get('p99', 0):,} | {adv.get('p95', 0):,} | {adv.get('median', 0):,} | {adv.get('exact', 0):,} | + +For an exact before/after comparison, the certificate also simulates the +replaced SCALE/Remez rational recurrence on these same vector files: + +| Kernel/corpus | Max | P99 | P95 | Median | Exact | +|---|---:|---:|---:|---:|---:| +| legacy / production | {legacy_prod.get('max', 0):,} | {legacy_prod.get('p99', 0):,} | {legacy_prod.get('p95', 0):,} | {legacy_prod.get('median', 0):,} | {legacy_prod.get('exact', 0):,} | +| N32/Q63 / production | {prod.get('max', 0):,} | {prod.get('p99', 0):,} | {prod.get('p95', 0):,} | {prod.get('median', 0):,} | {prod.get('exact', 0):,} | +| legacy / adversarial | {legacy_adv.get('max', 0):,} | {legacy_adv.get('p99', 0):,} | {legacy_adv.get('p95', 0):,} | {legacy_adv.get('median', 0):,} | {legacy_adv.get('exact', 0):,} | +| N32/Q63 / adversarial | {adv.get('max', 0):,} | {adv.get('p99', 0):,} | {adv.get('p95', 0):,} | {adv.get('median', 0):,} | {adv.get('exact', 0):,} | + +These corpora are empirical cross-checks and are not used to establish the +continuous theorem. + +## Scope + +This is a source-bound proof of the mathematical and exact-integer recurrence. +It does not prove Rust compiler, LLVM/SBF VM, operating-system or hardware +correctness. Deployed CU and linked-size measurements are separate artifacts. +""" + + +def build_certificate() -> dict[str, Any]: + binding, generator, constants, coefficients, phases = source_binding() + polynomial = certify_polynomial(generator, coefficients) + reduction = reduction_and_width_certificate(constants, coefficients, phases) + monotonicity = seam_and_monotonicity_certificate( + constants, coefficients, phases, reduction, polynomial + ) + errors = final_error_certificate(constants, phases, polynomial, reduction) + certificate = { + "schema": "solmath.exp_fixed_i.proof.v1", + "date": DATE, + "classification": "Arb interval proof plus exact integer/seam verification", + "source_binding": binding, + "constants": constants, + "polynomial": polynomial, + "reduction": reduction, + "monotonicity": monotonicity, + "error_bounds": errors, + "retained_corpora": { + "production": retained_corpus_stats( + PRODUCTION_VECTORS, constants, coefficients, phases + ), + "adversarial": retained_corpus_stats( + ADVERSARIAL_VECTORS, constants, coefficients, phases + ), + }, + "legacy_comparison": { + "description": ( + "Exact simulator for the replaced SCALE/Remez rational kernel, " + "evaluated on the identical retained vector files." + ), + "production": retained_corpus_stats( + PRODUCTION_VECTORS, constants, coefficients, phases, legacy=True + ), + "adversarial": retained_corpus_stats( + ADVERSARIAL_VECTORS, constants, coefficients, phases, legacy=True + ), + }, + "limitations": ( + "Source-bound numerical/integer proof; compiler, LLVM/SBF VM, OS and " + "hardware correctness are outside scope." + ), + } + return strip_private(certificate) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--check", action="store_true", help="compare with checked-in certificates") + parser.add_argument( + "--print-source-digests", action="store_true", help="print hashes even before pinning" + ) + args = parser.parse_args() + certificate = build_certificate() + if args.print_source_digests: + print(json.dumps(certificate["source_binding"], indent=2, sort_keys=True)) + return + json_text = json.dumps(certificate, indent=2, sort_keys=True) + "\n" + markdown_text = render_markdown(certificate) + if args.check: + assert JSON_OUTPUT.read_text() == json_text, f"{JSON_OUTPUT} is stale" + assert MARKDOWN_OUTPUT.read_text() == markdown_text, f"{MARKDOWN_OUTPUT} is stale" + print("verified exp_fixed_i proof certificates") + else: + JSON_OUTPUT.parent.mkdir(parents=True, exist_ok=True) + JSON_OUTPUT.write_text(json_text) + MARKDOWN_OUTPUT.write_text(markdown_text) + print(f"wrote {JSON_OUTPUT}") + print(f"wrote {MARKDOWN_OUTPUT}") + + +if __name__ == "__main__": + main() diff --git a/scripts/certify_ln_fixed.py b/scripts/certify_ln_fixed.py new file mode 100644 index 0000000..7e81c66 --- /dev/null +++ b/scripts/certify_ln_fixed.py @@ -0,0 +1,549 @@ +#!/usr/bin/env python3 +"""Rigorous all-input certificate for the current ``ln_fixed_i`` kernel. + +This is a proof checker, not a sampler. It combines exact integer/rational +arithmetic with Arb ball arithmetic (via python-flint) and covers every +``u128`` input symbolically by binary exponent and LUT segment. + +The checker is intentionally bound to the exact Rust functions and generated +tables that it proves. A change to one of those function bodies makes the +kernel digest assertion fail until the proof is reviewed and refreshed. + +Install the pinned proof dependency with:: + + python3 -m pip install python-flint==0.8.0 + +Then run:: + + python3 scripts/certify_ln_fixed.py + python3 scripts/certify_ln_fixed.py --json + +The certified error is relative to ``1e12 * ln(x / 1e12)``. The final ULP +claim compares the Rust integer result with that real value rounded to the +nearest integer (either tie rule is covered). +""" + +from __future__ import annotations + +import argparse +import hashlib +import importlib.metadata +import json +import re +from fractions import Fraction +from pathlib import Path + +try: + from flint import arb, ctx +except ImportError as exc: # pragma: no cover - dependency failure path + raise SystemExit( + "ln certificate requires python-flint==0.8.0; install it with " + "`python3 -m pip install python-flint==0.8.0`" + ) from exc + + +REPO = Path(__file__).resolve().parents[1] +TRANS_SOURCE = REPO / "src" / "transcendental.rs" +CONSTANTS_SOURCE = REPO / "src" / "constants.rs" +LN_LUT_SOURCE = REPO / "src" / "ln_lut.rs" +LN2_LUT_SOURCE = REPO / "src" / "ln2_lut.rs" + +PYTHON_FLINT_VERSION = "0.8.0" +ARB_DPS = 100 +EXPECTED_KERNEL_SHA256 = "8c1200e8c3caff7185a4d12051f999ab608952996adafc7094b5c92b571a6a8e" +KERNEL_FUNCTIONS = ( + "round_shift_signed", + "round_shift_i64", + "mul_q42", + "ln_mantissa_lut", + "normalize_ln_fallback", + "normalize_ln", + "ln_fixed_i", +) + +SCALE = 10**12 +Q42 = 1 << 42 +RECIP_GUARD = 1 << 32 +SEGMENTS = 1024 +STEP = SCALE // SEGMENTS +HALF_STEP = STEP // 2 +NEAR_ONE_RAW = 1_000_000 +REACHABLE_K_MIN = -40 +REACHABLE_K_MAX = 88 +I64_MAX = (1 << 63) - 1 +I128_MAX = (1 << 127) - 1 + +# Outward-rounded public certificate thresholds. Every candidate is checked +# against these values with Arb interval comparisons; the long diagnostic +# enclosures printed by the script are evidence, not assumptions. +ctx.dps = ARB_DPS +REGULAR_REAL_ERROR_LT = arb("2.925564") +SPECIAL_REAL_ERROR_LT = arb("1.499803") +NEAR_ONE_REAL_ERROR_LT = arb("0.5") +ROUNDED_ULP_LE = 3 + + +def sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def extract_function(source: str, name: str) -> str: + """Extract one Rust function body, including its signature.""" + + match = re.search(rf"(?m)^(?:pub )?fn {re.escape(name)}\b", source) + if match is None: + raise AssertionError(f"missing Rust function {name}") + brace = source.index("{", match.start()) + depth = 0 + for index in range(brace, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[match.start() : index + 1] + raise AssertionError(f"unterminated Rust function {name}") + + +def kernel_sha256() -> str: + source = TRANS_SOURCE.read_text() + bound_source = "\n".join( + f"{name}\0{extract_function(source, name)}" for name in KERNEL_FUNCTIONS + ) + return hashlib.sha256(bound_source.encode()).hexdigest() + + +def parse_rust_array(path: Path, name: str) -> list[int]: + source = path.read_text() + match = re.search( + rf"const\s+{re.escape(name)}\s*:.*?=\s*\[(.*?)\];", + source, + flags=re.DOTALL, + ) + if match is None: + raise AssertionError(f"missing Rust array {name}") + return [int(value) for value in re.findall(r"-?\d+", match.group(1))] + + +def parse_rust_integer(path: Path, name: str) -> int: + source = path.read_text() + match = re.search( + rf"const\s+{re.escape(name)}\s*:.*?=\s*(-?[\d_]+)" + rf"(?:u128|i128|u64|i64|usize|i32)?\s*;", + source, + ) + if match is None: + raise AssertionError(f"missing Rust integer {name}") + return int(match.group(1).replace("_", "")) + + +def round_nearest_away(numerator: int, denominator: int) -> int: + """Exact model of the signed Rust round-shift helpers.""" + + assert denominator > 0 and denominator % 2 == 0 + half = denominator // 2 + if numerator >= 0: + return (numerator + half) // denominator + return -((-numerator + half) // denominator) + + +def arb_fraction(value: Fraction) -> arb: + return arb(value.numerator) / value.denominator + + +def strict_less(left: arb, right: arb, description: str) -> None: + if not left < right: + raise AssertionError(f"could not prove {description}: {left} < {right}") + + +def prove_normalization() -> tuple[int, int, int]: + """Cover the fallback's 128 bit-length classes by monotone intervals. + + For a bit length ``b``, the initial exponent is ``b - 40`` and the first + shifted mantissa is in ``[2^39, 2^40)``. The only possible correction is + therefore the ``m < SCALE`` branch. The intervals below split exactly at + that branch condition and check their endpoint images; monotonicity of a + shift proves every integer inside each interval has the same bounds. + """ + + exponents: list[int] = [] + intervals = 0 + for bit_length in range(1, 129): + x_low = 1 << (bit_length - 1) + x_high = (1 << bit_length) - 1 + k0 = bit_length - 40 + covered = 0 + + if k0 >= 0: + divisor = 1 << k0 + low_branch_high = min(x_high, SCALE * divisor - 1) + high_branch_low = max(x_low, SCALE * divisor) + + if x_low <= low_branch_high: + k = k0 - 1 + assert k >= 0 or bit_length <= 40 + if k >= 0: + m_low = x_low >> k + m_high = low_branch_high >> k + else: + m_low = x_low << -k + m_high = low_branch_high << -k + assert SCALE <= m_low <= m_high < 2 * SCALE + exponents.append(k) + intervals += 1 + covered += low_branch_high - x_low + 1 + + if high_branch_low <= x_high: + k = k0 + m_low = high_branch_low >> k + m_high = x_high >> k + assert SCALE <= m_low <= m_high < 2 * SCALE + exponents.append(k) + intervals += 1 + covered += x_high - high_branch_low + 1 + + if x_low <= low_branch_high and high_branch_low <= x_high: + assert low_branch_high + 1 == high_branch_low + else: + multiplier = 1 << -k0 + low_branch_high = min(x_high, (SCALE - 1) // multiplier) + high_branch_low = max(x_low, (SCALE + multiplier - 1) // multiplier) + + if x_low <= low_branch_high: + k = k0 - 1 + m_low = x_low << -k + m_high = low_branch_high << -k + assert SCALE <= m_low <= m_high < 2 * SCALE + exponents.append(k) + intervals += 1 + covered += low_branch_high - x_low + 1 + + if high_branch_low <= x_high: + k = k0 + m_low = high_branch_low << -k + m_high = x_high << -k + assert SCALE <= m_low <= m_high < 2 * SCALE + exponents.append(k) + intervals += 1 + covered += x_high - high_branch_low + 1 + + if x_low <= low_branch_high and high_branch_low <= x_high: + assert low_branch_high + 1 == high_branch_low + + assert covered == x_high - x_low + 1 + + assert min(exponents) == REACHABLE_K_MIN + assert max(exponents) == REACHABLE_K_MAX + assert all(REACHABLE_K_MIN <= k <= REACHABLE_K_MAX for k in exponents) + + # The two hot branches bypass the fallback but are the same normalized + # relation: (x,0) on [S,2S), and (2x,-1) on [S/2,S). + assert SCALE < 2 * SCALE <= (1 << 128) + assert 2 * (SCALE // 2) == SCALE + assert SCALE <= 2 * (SCALE - 1) < 2 * SCALE + return min(exponents), max(exponents), intervals + + +def main() -> dict[str, object]: + installed = importlib.metadata.version("python-flint") + if installed != PYTHON_FLINT_VERSION: + raise AssertionError( + f"proof requires python-flint {PYTHON_FLINT_VERSION}, found {installed}" + ) + + # 100 decimal digits is far more than needed to separate every rounding + # boundary in the current tables. Arb propagates a rigorous radius. + ctx.dps = ARB_DPS + + digest = kernel_sha256() + assert digest == EXPECTED_KERNEL_SHA256, ( + "the certified ln Rust functions changed; review the proof and refresh " + f"EXPECTED_KERNEL_SHA256 (found {digest})" + ) + + # SCALE and SCALE_I are imported into the certified functions from a + # separate module, so bind their values explicitly as part of the proof. + assert parse_rust_integer(CONSTANTS_SOURCE, "SCALE") == SCALE + assert parse_rust_integer(CONSTANTS_SOURCE, "SCALE_I") == SCALE + assert parse_rust_integer(LN_LUT_SOURCE, "LN_LUT_SEGMENTS") == SEGMENTS + assert parse_rust_integer(LN_LUT_SOURCE, "LN_LUT_STEP") == STEP + assert parse_rust_integer(LN_LUT_SOURCE, "LN_LUT_HALF_STEP") == HALF_STEP + assert parse_rust_integer(LN2_LUT_SOURCE, "K_LN2_MIN") == -64 + assert parse_rust_integer(LN2_LUT_SOURCE, "K_LN2_MAX") == 88 + + midpoint_logs = parse_rust_array(LN_LUT_SOURCE, "LN_LUT_MID_LOG") + reciprocals = parse_rust_array(LN_LUT_SOURCE, "LN_Q42_RECIP_G32") + k_ln2 = parse_rust_array(LN2_LUT_SOURCE, "K_LN2_RAW") + assert len(midpoint_logs) == len(reciprocals) == SEGMENTS + assert len(k_ln2) == 153 + + k_min, k_max, normalization_intervals = prove_normalization() + + half = arb(1) / 2 + ln2 = arb(2).log() + table_errors: list[arb] = [] + for j, stored in enumerate(midpoint_logs): + midpoint = SCALE + j * STEP + HALF_STEP + truth = arb(SCALE) * (arb(midpoint) / SCALE).log() + strict_less(arb(stored) - half, truth, f"midpoint[{j}] lower rounding edge") + strict_less(truth, arb(stored) + half, f"midpoint[{j}] upper rounding edge") + table_errors.append(arb(stored) - truth) + + # The reciprocal is nearest-integer Q42 with 32 extra guard bits. + reciprocal_residual = abs( + reciprocals[j] * midpoint - Q42 * RECIP_GUARD + ) + assert 2 * reciprocal_residual <= midpoint + + k_errors: dict[int, arb] = {} + for index, stored in enumerate(k_ln2): + k = index - 64 + truth = arb(k * SCALE) * ln2 + strict_less(arb(stored) - half, truth, f"k_ln2[{k}] lower rounding edge") + strict_less(truth, arb(stored) + half, f"k_ln2[{k}] upper rounding edge") + if REACHABLE_K_MIN <= k <= REACHABLE_K_MAX: + k_errors[k] = arb(stored) - truth + + # Near-one branch. The error is monotone in |x-SCALE| on each side, and + # the negative side is the larger endpoint. + near_delta = NEAR_ONE_RAW - 1 + near_positive = arb(near_delta) - arb(SCALE) * ( + arb(1) + arb(near_delta) / SCALE + ).log() + near_negative = -arb(near_delta) - arb(SCALE) * ( + arb(1) - arb(near_delta) / SCALE + ).log() + near_error = near_positive.max(near_negative) + strict_less(near_error, NEAR_ONE_REAL_ERROR_LT, "near-one real error") + strict_less(near_error + half, arb(4), "near-one correctly-rounded ULP < 4") + + max_d_times_recip = 0 + max_q_abs = 0 + max_q_square = 0 + max_q2_times_q = 0 + max_local_times_scale = 0 + largest_local_bound = arb(0) + largest_regular_bound = arb(0) + regular_maximizer = (0, 0) + regular_maximizer_midpoint = -1.0 + + for j, (stored_log, reciprocal, table_error) in enumerate( + zip(midpoint_logs, reciprocals, table_errors) + ): + midpoint = SCALE + j * STEP + HALF_STEP + d_low = -HALF_STEP + (1 if j == 0 else 0) # m=SCALE is special-cased + d_high = HALF_STEP - 1 + d_abs = max(abs(d_low), abs(d_high)) + + endpoint_q = [ + round_nearest_away(d_low * reciprocal, RECIP_GUARD), + round_nearest_away(d_high * reciprocal, RECIP_GUARD), + ] + q_abs = max(abs(q) for q in endpoint_q) + q_ratio = Fraction(q_abs, Q42) + t_ratio = Fraction(d_abs, midpoint) + z_ratio = max(q_ratio, t_ratio) + + reciprocal_residual = abs(reciprocal * midpoint - Q42 * RECIP_GUARD) + q_error_units = Fraction(1, 2) + Fraction( + d_abs * reciprocal_residual, midpoint * RECIP_GUARD + ) + + # Integer cubic evaluation. q2 contributes at most 3/4 Q42 unit + # after /2. q3 contributes at most 5/6 + |q|/(6Q42) after /3. + integer_cubic_units = Fraction(19, 12) + q_ratio / 6 + + # |p'(z)| for p(z)=z-z^2/2+z^3/3 is <= 1+Z+Z^2 on + # the interval connecting the exact d/midpoint and quantized q/Q42. + cubic_derivative = 1 + z_ratio + z_ratio * z_ratio + + # The log-series tail is sum_{n>=4} (-1)^(n+1)t^n/n. + series_tail = ( + SCALE * t_ratio**4 / (4 * (1 - t_ratio)) + ) + local_bound = ( + Fraction(1, 2) + + Fraction(SCALE, Q42) * integer_cubic_units + + Fraction(SCALE, Q42) * cubic_derivative * q_error_units + + series_tail + ) + local_bound_arb = arb_fraction(local_bound) + largest_local_bound = largest_local_bound.max(local_bound_arb) + + # Overflow proof. Endpoint checks suffice because q is a monotone + # rounded linear function of d inside each segment. + d_times_recip = max(abs(d_low * reciprocal), abs(d_high * reciprocal)) + q2_abs = round_nearest_away(q_abs * q_abs, Q42) + q3_product_abs = q2_abs * q_abs + q3_abs = round_nearest_away(q3_product_abs, Q42) + local_abs = q_abs + (q2_abs + 1) // 2 + (q3_abs + 2) // 3 + max_d_times_recip = max(max_d_times_recip, d_times_recip) + max_q_abs = max(max_q_abs, q_abs) + max_q_square = max(max_q_square, q_abs * q_abs) + max_q2_times_q = max(max_q2_times_q, q3_product_abs) + max_local_times_scale = max(max_local_times_scale, local_abs * SCALE) + + assert d_times_recip <= I64_MAX + assert q_abs * q_abs <= I64_MAX + assert q3_product_abs <= I64_MAX + assert local_abs * SCALE <= I128_MAX + # The helpers add half a denominator before shifting. Check those + # intermediate numerators too, including the negation path: none of + # the certified values can be the signed minimum, so abs(value)+half + # covers both signs exactly. + assert d_times_recip + RECIP_GUARD // 2 <= I64_MAX + assert q_abs * q_abs + Q42 // 2 <= I64_MAX + assert q3_product_abs + Q42 // 2 <= I64_MAX + assert local_abs * SCALE + Q42 // 2 <= I128_MAX + assert local_abs <= I64_MAX + assert 0 <= j < SEGMENTS + assert -(1 << 63) <= midpoint + d_low <= midpoint + d_high <= I64_MAX + + # For k>0, x=m*2^k+r and normalization discards r. This contributes + # N=S*ln(1+r/(m*2^k)), bounded at the smallest regular m in the + # segment and r=2^k-1. For k<=0 normalization is exact. + for k in range(REACHABLE_K_MIN, REACHABLE_K_MAX + 1): + constant_error = table_error + k_errors[k] + constant_and_normalization = abs(constant_error).upper() + if k > 0: + m_min = SCALE + j * STEP + (1 if j == 0 else 0) + normalization_max = arb(SCALE) * ( + arb(1) + + arb((1 << k) - 1) / arb(m_min * (1 << k)) + ).log() + other_endpoint = abs(constant_error - normalization_max).upper() + constant_and_normalization = constant_and_normalization.max( + other_endpoint + ) + + total_bound = constant_and_normalization + local_bound_arb + strict_less( + total_bound, + REGULAR_REAL_ERROR_LT, + f"regular real error at segment={j}, k={k}", + ) + largest_regular_bound = largest_regular_bound.max(total_bound) + midpoint_float = float(total_bound) + if midpoint_float > regular_maximizer_midpoint: + regular_maximizer_midpoint = midpoint_float + regular_maximizer = (j, k) + + # m=SCALE bypasses the midpoint table and cubic. Only the pre-rounded + # whole k*ln(2) constant and (for k>0) discarded normalization bits remain. + largest_special_bound = arb(0) + special_maximizer = 0 + special_maximizer_midpoint = -1.0 + for k in range(REACHABLE_K_MIN, REACHABLE_K_MAX + 1): + special_bound = abs(k_errors[k]).upper() + if k > 0: + normalization_max = arb(SCALE) * ( + arb(1) + arb((1 << k) - 1) / arb(SCALE * (1 << k)) + ).log() + special_bound = special_bound.max( + abs(k_errors[k] - normalization_max).upper() + ) + strict_less( + special_bound, + SPECIAL_REAL_ERROR_LT, + f"m=SCALE real error at k={k}", + ) + largest_special_bound = largest_special_bound.max(special_bound) + midpoint_float = float(special_bound) + if midpoint_float > special_maximizer_midpoint: + special_maximizer_midpoint = midpoint_float + special_maximizer = k + + strict_less( + largest_special_bound + half, + arb(4), + "m=SCALE correctly-rounded ULP < 4", + ) + + # Nearest-integer reference error is <= 0.5. Since both Rust and the + # reference are integers, a strict difference below 4 proves <= 3 ULP. + rounded_reference_bound = largest_regular_bound + half + strict_less(rounded_reference_bound, arb(4), "correctly-rounded ULP < 4") + + max_output_abs = ( + max(abs(value) for value in midpoint_logs) + + max(abs(value) for value in k_ln2) + + (max_local_times_scale + Q42 // 2) // Q42 + ) + assert max_output_abs <= I128_MAX + + return { + "schema": "solmath-ln-fixed-certificate-v1", + "checker_sha256": sha256(Path(__file__).resolve()), + "proof_engine": f"python-flint {installed} / Arb at {ctx.dps} decimal digits", + "kernel_function_sha256": digest, + "source_sha256": { + "src/transcendental.rs": sha256(TRANS_SOURCE), + "src/constants.rs": sha256(CONSTANTS_SOURCE), + "src/ln_lut.rs": sha256(LN_LUT_SOURCE), + "src/ln2_lut.rs": sha256(LN2_LUT_SOURCE), + }, + "domain": { + "valid_x": "1..=u128::MAX", + "x_zero": "DomainError", + "normalization_k": [k_min, k_max], + "normalization_monotone_intervals": normalization_intervals, + "segments": SEGMENTS, + "segment_exponent_pairs_checked": SEGMENTS + * (REACHABLE_K_MAX - REACHABLE_K_MIN + 1), + }, + "table_checks": { + "midpoint_logs_correctly_rounded": len(midpoint_logs), + "reciprocals_nearest_integer": len(reciprocals), + "k_ln2_correctly_rounded": len(k_ln2), + }, + "real_error": { + "near_one_computed_enclosure": str(near_error), + "near_one_proved_lt": "0.5", + "m_equals_scale_computed_enclosure": str(largest_special_bound), + "m_equals_scale_maximizer_k": special_maximizer, + "m_equals_scale_proved_lt": "1.499803", + "largest_local_computed_enclosure": str(largest_local_bound), + "regular_computed_enclosure": str(largest_regular_bound), + "regular_maximizer_segment_k": list(regular_maximizer), + "regular_proved_lt": "2.925564", + }, + "correctly_rounded_reference": { + "computed_triangle_enclosure": str(rounded_reference_bound), + "integer_ulp_bound": ROUNDED_ULP_LE, + }, + "overflow_maxima": { + "abs_d_times_recip_i64": max_d_times_recip, + "abs_d_times_recip_plus_rounding_half_i64": max_d_times_recip + + RECIP_GUARD // 2, + "abs_q_q42": max_q_abs, + "q_square_i64": max_q_square, + "q_square_plus_rounding_half_i64": max_q_square + Q42 // 2, + "abs_q2_times_q_i64": max_q2_times_q, + "abs_q2_times_q_plus_rounding_half_i64": max_q2_times_q + Q42 // 2, + "abs_local_times_scale_i128": max_local_times_scale, + "abs_local_times_scale_plus_rounding_half_i128": max_local_times_scale + + Q42 // 2, + "abs_return_conservative_i128": max_output_abs, + }, + "result": "PASS", + } + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--json", action="store_true", help="emit the certificate as JSON") + args = parser.parse_args() + certificate = main() + if args.json: + print(json.dumps(certificate, indent=2, sort_keys=True)) + else: + error = certificate["real_error"] + rounded = certificate["correctly_rounded_reference"] + print("ln_fixed_i all-input certificate: PASS") + print(f" kernel sha256: {certificate['kernel_function_sha256']}") + print(f" regular real error: < {error['regular_proved_lt']} raw units") + print(f" near-one real error: < {error['near_one_proved_lt']} raw units") + print( + " correctly-rounded reference: <= " + f"{rounded['integer_ulp_bound']} ULP" + ) diff --git a/scripts/certify_norm_cdf.py b/scripts/certify_norm_cdf.py new file mode 100644 index 0000000..a579a9f --- /dev/null +++ b/scripts/certify_norm_cdf.py @@ -0,0 +1,814 @@ +#!/usr/bin/env python3 +"""Rigorous certificate for the current Q44/Q23 ``norm_cdf_poly`` kernel. + +This script reads the coefficients and fixed-point parameters from the Rust +sources, then proves four separate claims: + +1. each exact, quantized polynomial approximates the corresponding normal CDF + (or survival probability) on its complete continuous interval; +2. Q44 coordinate mapping and rounded integer Horner evaluation preserve an + all-input error bound below 2.5 raw output units; +3. every dispatch seam and the half-raw tail cutoff are correctly ordered; and +4. the actual integer output is nondecreasing over the complete discrete i128 + input domain. + +Continuous bounds use Arb balls (python-flint). They are not sampling claims: +sampled values are elevated to interval-wide bounds with the mean-value +theorem, using a purely analytical third-derivative majorant. Discrete +monotonicity is reduced to a finite set of possibly ambiguous final-rounding +cells. A generated Rust verifier exhausts every raw input pair in those cells +using the same i128 rounding recurrences as the production kernel. + +Reproduce with: + + python3 -m pip install --target /tmp/solmath-proof-deps python-flint==0.8.0 + PYTHONPATH=/tmp/solmath-proof-deps python3 scripts/certify_norm_cdf.py + +The generated verifier and candidate-cell file live in a temporary directory; +the repository is not modified by running this certificate. +""" + +from __future__ import annotations + +import argparse +import hashlib +import math +import re +import struct +import subprocess +import tempfile +import time +from dataclasses import dataclass +from pathlib import Path + +try: + import flint + from flint import arb, ctx, fmpq +except ImportError as exc: # pragma: no cover - exercised by dependency failure + raise SystemExit( + "python-flint==0.8.0 is required; install it into an isolated directory " + "and put that directory on PYTHONPATH" + ) from exc + + +ROOT = Path(__file__).resolve().parents[1] +COEFF_SOURCE = ROOT / "src" / "norm_cdf_coeffs.rs" +KERNEL_SOURCE = ROOT / "src" / "normal.rs" +CONSTANT_SOURCE = ROOT / "src" / "constants.rs" +SCALE = 10**12 +Q = 1 << 44 +GUARD = 1 << 23 +TAIL_EXTRA_Q = 16 +HW_RAW = SCALE // 4 +RECIPROCAL = 1_237_940_039_285_380 +TAIL_CUTOFF = 7_130_506_848_171 +ARB_BITS = 256 +ERROR_GRID = 100_000 +DERIVATIVE_GRID = 100_000 +SECOND_GRID = 10_000 +SIGN_GRID = 4096 +EXPECTED_COEFFICIENT_SHA256 = "12e6200c0985ebc9f5a73b3a6585ca1c294a0ec34b1af85338e44f2bc41340ef" +EXPECTED_MODELED_KERNEL_SHA256 = "dece876f81c6e35c67768878ef89eb88429c8e04beb63a64996e5f9e68f78994" +MODELED_FUNCTIONS = ( + "round_shift_cdf", + "horner_guard_q44", + "horner_tail_guard_q44", + "poly_map_t_q44", + "norm_cdf_positive_tail", + "norm_cdf_poly", +) + + +@dataclass(frozen=True) +class Piece: + name: str + lo_num: int # exact sigma endpoint numerator over 2 + hi_num: int + tail: bool + coefficients: tuple[int, ...] + + @property + def lo_raw(self) -> int: + return self.lo_num * SCALE // 2 + + @property + def hi_raw(self) -> int: + return self.hi_num * SCALE // 2 + + @property + def mid_raw(self) -> int: + return (self.lo_raw + self.hi_raw) // 2 + + @property + def degree(self) -> int: + return max(i for i, value in enumerate(self.coefficients) if value != 0) + + +PIECE_LAYOUT = ( + ("NORM_CDF_0_05_Q23", 0, 1, False), + ("NORM_CDF_05_10_Q23", 1, 2, False), + ("NORM_CDF_10_15_Q23", 2, 3, False), + ("NORM_CDF_15_20_Q23", 3, 4, False), + ("NORM_CDF_20_25_Q23", 4, 5, False), + ("NORM_CDF_25_30_Q23", 5, 6, False), + ("NORM_CDF_30_35_Q23", 6, 7, False), + ("NORM_CDF_35_40_Q23", 7, 8, False), + ("NORM_CDF_40_45_Q23", 8, 9, False), + ("NORM_CDF_45_50_Q23", 9, 10, False), + ("NORM_TAIL_50_55_Q23", 10, 11, True), + ("NORM_TAIL_55_60_Q23", 11, 12, True), + ("NORM_TAIL_60_65_Q23", 12, 13, True), + ("NORM_TAIL_65_70_Q23", 13, 14, True), +) + + +def extract_rust_function(source: str, name: str) -> str: + """Extract one complete Rust function, including its inline attribute.""" + match = re.search( + rf"(?m)^(?:pub\(crate\) |pub )?(?:#\[[^\n]+\]\n)*fn {name}" + rf"(?:<[^\n]+>)?\s*\(", + source, + ) + assert match, f"cannot find modeled Rust function {name}" + brace = source.find("{", match.end()) + assert brace >= 0 + depth = 0 + for index in range(brace, len(source)): + if source[index] == "{": + depth += 1 + elif source[index] == "}": + depth -= 1 + if depth == 0: + return source[match.start() : index + 1] + raise AssertionError(f"unterminated modeled Rust function {name}") + + +def parse_sources() -> tuple[list[Piece], str, str, str]: + coefficient_text = COEFF_SOURCE.read_text() + kernel_text = KERNEL_SOURCE.read_text() + constant_text = CONSTANT_SOURCE.read_text() + arrays: dict[str, tuple[int, ...]] = {} + pattern = re.compile( + r"const (NORM_(?:CDF|TAIL)_[A-Z0-9_]+): \[i64; \d+\] = \[(.*?)\];", + re.S, + ) + for name, body in pattern.findall(coefficient_text): + arrays[name] = tuple( + int(token.replace("_", "")) + for token in re.findall(r"-?[0-9][0-9_]*", body) + ) + + assert "pub(crate) const CDF_T_Q: u32 = 44;" in coefficient_text + assert "pub(crate) const CDF_COEFF_GUARD_Q: u32 = 23;" in coefficient_text + assert "pub(crate) const CDF_TAIL_EVAL_EXTRA_Q: u32 = 16;" in coefficient_text + cutoff_match = re.search( + r"NORM_TAIL_HALF_RAW_CUTOFF: i128 = ([0-9_]+);", coefficient_text + ) + assert cutoff_match + assert int(cutoff_match.group(1).replace("_", "")) == TAIL_CUTOFF + assert "round_shift_cdf(result as i128 * t as i128) + coefficient" in kernel_text + assert "tail.clamp(0, SCALE_I as i64)" in kernel_text + assert "if x < -8 * SCALE_I" in kernel_text + assert "if x > 8 * SCALE_I" in kernel_text + + # Bind the certificate to the exact production recurrences and dispatch. + # A source edit must deliberately update this digest and rerun the proof; + # a few token-level assertions are not accepted as equivalence evidence. + modeled_kernel = "\n\n".join( + extract_rust_function(kernel_text, name) for name in MODELED_FUNCTIONS + ) + "\n" + assert sha256_text(coefficient_text) == EXPECTED_COEFFICIENT_SHA256 + assert sha256_text(modeled_kernel) == EXPECTED_MODELED_KERNEL_SHA256 + scale_match = re.search(r"pub const SCALE_I: i128 = ([0-9_]+)i128;", constant_text) + assert scale_match + assert int(scale_match.group(1).replace("_", "")) == SCALE + + pieces = [ + Piece(name, lo, hi, tail, arrays[name]) + for name, lo, hi, tail in PIECE_LAYOUT + ] + return pieces, coefficient_text, kernel_text, modeled_kernel + + +def certify_integer_safety(pieces: list[Piece]) -> None: + """Machine-check every width assumption used by the integer model.""" + i64_max = (1 << 63) - 1 + i128_max = (1 << 127) - 1 + i128_min = -(1 << 127) + q_half = Q // 2 + + # The public guards execute before x.abs(). In particular i128::MIN takes + # the first return, while every value reaching abs is in [-8S, 8S]. + assert i128_min < -8 * SCALE < 8 * SCALE < i128_max + assert abs(-8 * SCALE) < i128_max + + # Every current dispatch call uses |ax-mid| <= SCALE/4. The mapping + # multiplication, sign-aware negation, and half addition all fit i128. + assert max(abs(piece.lo_raw - piece.mid_raw) for piece in pieces) == HW_RAW + assert max(abs(piece.hi_raw - piece.mid_raw) for piece in pieces) == HW_RAW + map_product = HW_RAW * RECIPROCAL + assert map_product + q_half <= i128_max + assert -map_product > i128_min + assert -i64_max > -(1 << 63) + for piece in pieces: + assert map_t(piece.lo_raw, piece.mid_raw) == -Q + assert map_t(piece.hi_raw, piece.mid_raw) == Q + + # With |t| <= Q, rounded multiplication cannot increase accumulator + # magnitude. Summed absolute coefficients therefore bound every cast, + # add, final rounding, and i128 multiplication in every Horner step. + extra_q = TAIL_EXTRA_Q if piece.tail else 0 + bound = abs(piece.coefficients[-1]) << extra_q + assert bound <= i64_max + for coefficient in reversed(piece.coefficients[:-1]): + assert bound * Q + q_half <= i128_max + assert bound <= i64_max # round_shift_cdf result -> i64 cast + bound += abs(coefficient) << extra_q + assert bound <= i64_max # i64 coefficient addition + output_guard = GUARD << extra_q + assert bound + output_guard // 2 <= i128_max + assert bound + output_guard // 2 <= i64_max + assert (bound + output_guard // 2) // output_guard <= i64_max + + # Tail subtraction, clamp/cast, symmetry, and all midpoint expressions fit. + assert 0 <= SCALE <= i64_max + assert 27 * SCALE <= i128_max + + +def rn_div(value: int, divisor: int) -> int: + """Round to nearest with half ties away from zero, exactly as Rust.""" + if value >= 0: + return (value + divisor // 2) // divisor + return -((-value + divisor // 2) // divisor) + + +def map_t(raw_x: int, midpoint: int) -> int: + return rn_div((raw_x - midpoint) * RECIPROCAL, Q) + + +def horner_integer(coefficients: tuple[int, ...], t: int, extra_q: int = 0) -> int: + result = coefficients[-1] << extra_q + for coefficient in reversed(coefficients[:-1]): + result = rn_div(result * t, Q) + (coefficient << extra_q) + return result + + +def rounded_piece_value(piece: Piece, raw_x: int) -> int: + extra_q = TAIL_EXTRA_Q if piece.tail else 0 + guarded = horner_integer( + piece.coefficients, map_t(raw_x, piece.mid_raw), extra_q + ) + return rn_div(guarded, GUARD << extra_q) + + +def evaluation_guard(piece: Piece) -> int: + return GUARD << (TAIL_EXTRA_Q if piece.tail else 0) + + +def arb_poly(coefficients: tuple[int, ...], t: arb, derivative: int = 0) -> arb: + derived = list(coefficients) + for _ in range(derivative): + derived = [i * derived[i] for i in range(1, len(derived))] + if not derived: + return arb(0) + result = arb(derived[-1]) / GUARD + for coefficient in reversed(derived[:-1]): + result = result * t + arb(coefficient) / GUARD + return result + + +def phi(x: arb) -> arb: + return (-x * x / 2).exp() / (arb(2) * arb.pi()).sqrt() + + +def target(piece: Piece, t: arb, derivative: int = 0) -> arb: + midpoint = arb(piece.lo_num + piece.hi_num) / 4 + half_width = arb(piece.hi_num - piece.lo_num) / 4 + x = midpoint + half_width * t + scale = arb(SCALE) + sign = -1 if piece.tail else 1 + if derivative == 0: + if piece.tail: + return scale * (x / arb(2).sqrt()).erfc() / 2 + return scale * (arb(1) + (x / arb(2).sqrt()).erf()) / 2 + density = phi(x) + if derivative == 1: + return sign * scale * half_width * density + if derivative == 2: + return -sign * scale * half_width**2 * x * density + if derivative == 3: + return sign * scale * half_width**3 * (x * x - 1) * density + raise ValueError("only derivatives 0..3 are used") + + +def upper_abs(value: arb) -> arb: + return abs(value).abs_upper() + + +def grid_max(piece: Piece, derivative: int, count: int) -> arb: + maximum = arb(0) + for index in range(count + 1): + t = arb(-1) + arb(2 * index) / count + error = arb_poly(piece.coefficients, t, derivative) - target( + piece, t, derivative + ) + maximum = maximum.max(upper_abs(error)) + return maximum + + +def analytical_third_bound(piece: Piece) -> arb: + coefficients = piece.coefficients + polynomial = arb(0) + for i in range(3, len(coefficients)): + polynomial += arb(abs(i * (i - 1) * (i - 2) * coefficients[i])) / GUARD + + lo = arb(piece.lo_num) / 2 + hi = arb(piece.hi_num) / 2 + lo_x2_minus_one = abs(fmpq(piece.lo_num * piece.lo_num, 4) - 1) + hi_x2_minus_one = abs(fmpq(piece.hi_num * piece.hi_num, 4) - 1) + max_x2_minus_one = ( + lo_x2_minus_one + if lo_x2_minus_one > hi_x2_minus_one + else hi_x2_minus_one + ) + # phi is decreasing for x >= 0, so phi(lo) is a rigorous interval-wide max. + target_bound = ( + arb(SCALE) + * (arb(1) / 4) ** 3 + * (arb(int(max_x2_minus_one.numerator)) / int(max_x2_minus_one.denominator)) + * phi(lo) + ) + del hi # documents that the complete interval was considered above + return polynomial + target_bound.abs_upper() + + +def derivative_sign_bound(piece: Piece) -> arb: + """Prove the exact polynomial has the required sign on all t in [-1, 1].""" + minimum_oriented = None + orientation = -1 if piece.tail else 1 + for index in range(SIGN_GRID): + lo = arb(-1) + arb(2 * index) / SIGN_GRID + hi = arb(-1) + arb(2 * (index + 1)) / SIGN_GRID + interval = lo.union(hi) + oriented = orientation * arb_poly(piece.coefficients, interval, 1) + assert oriented > 0, f"unproved derivative sign in {piece.name} cell {index}" + lower = oriented.lower() + minimum_oriented = lower if minimum_oriented is None else minimum_oriented.min(lower) + assert minimum_oriented is not None and minimum_oriented > 0 + return minimum_oriented + + +@dataclass +class PieceCertificate: + piece: Piece + approximation: arb + map_error: arb + integer_error: arb + total_error: arb + derivative_min_guard: arb + monotone_by_margin: bool + + +def certify_piece(piece: Piece) -> PieceCertificate: + m3 = analytical_third_bound(piece) + second_sample = grid_max(piece, 2, SECOND_GRID) + m2 = second_sample + m3 / SECOND_GRID + derivative_sample = grid_max(piece, 1, DERIVATIVE_GRID) + lipschitz = derivative_sample + m2 / DERIVATIVE_GRID + error_sample = grid_max(piece, 0, ERROR_GRID) + approximation = error_sample + lipschitz / ERROR_GRID + + derivative_min_raw = derivative_sign_bound(piece) + eval_guard = evaluation_guard(piece) + derivative_min_guard = derivative_min_raw * eval_guard + + reciprocal_residual = abs(RECIPROCAL * HW_RAW - Q * Q) + map_delta_t = (arb(1) / 2 + arb(reciprocal_residual) / Q) / Q + polynomial_derivative_bound = arb(0) + for i in range(1, len(piece.coefficients)): + polynomial_derivative_bound += arb(i * abs(piece.coefficients[i])) / GUARD + map_error = polynomial_derivative_bound * map_delta_t + + integer_error = arb(1) / 2 + arb(piece.degree) / (2 * eval_guard) + total = approximation + map_error + integer_error + + minimum_t_step = RECIPROCAL // Q + guarded_step_lower = derivative_min_guard * minimum_t_step / Q + monotone_by_margin = guarded_step_lower > piece.degree + return PieceCertificate( + piece, + approximation, + map_error, + integer_error, + total, + derivative_min_guard, + monotone_by_margin, + ) + + +def exact_poly_num_den(piece: Piece, t: int) -> tuple[int, int]: + """Exact rational guarded polynomial at z=t/Q.""" + numerator = piece.coefficients[piece.degree] + denominator = 1 + for coefficient in reversed(piece.coefficients[: piece.degree]): + numerator = numerator * t + coefficient * denominator * Q + denominator *= Q + return numerator, denominator + + +def exact_poly_fmpq(piece: Piece, raw_x: int) -> fmpq: + """Exact guarded polynomial, evaluated by FLINT rational arithmetic.""" + z = fmpq(map_t(raw_x, piece.mid_raw), Q) + result = fmpq(piece.coefficients[piece.degree]) + for coefficient in reversed(piece.coefficients[: piece.degree]): + result = result * z + coefficient + return result + + +def compare_poly_to_twice_guarded(piece: Piece, raw_x: int, rhs_twice: int) -> int: + """Compare exact polynomial in evaluation-guard units with rhs_twice / 2.""" + extra_scale = 1 << (TAIL_EXTRA_Q if piece.tail else 0) + value = exact_poly_fmpq(piece, raw_x) * extra_scale + difference = 2 * value - rhs_twice + return (difference > 0) - (difference < 0) + + +def float_poly(piece: Piece, t: float) -> tuple[float, float]: + coefficients = [value / GUARD for value in piece.coefficients[: piece.degree + 1]] + value = coefficients[-1] + derivative = 0.0 + for coefficient in reversed(coefficients[:-1]): + derivative = derivative * t + value + value = value * t + coefficient + return value, derivative + + +def candidate_intervals( + piece_index: int, + certificate: PieceCertificate, +) -> tuple[list[tuple[int, int, int]], int]: + """Return every raw interval where final rounding could reverse direction.""" + piece = certificate.piece + degree = piece.degree + # Every production polynomial owns (lo, hi]. The exact seam at lo belongs + # to the preceding piece (and x=0 has a special return); certify_seams + # checks those cross-dispatch pairs separately. + lo = piece.lo_raw + 1 + hi = piece.hi_raw + extra_scale = 1 << (TAIL_EXTRA_Q if piece.tail else 0) + eval_guard = evaluation_guard(piece) + lo_exact = exact_poly_fmpq(piece, lo) * extra_scale + hi_exact = exact_poly_fmpq(piece, hi) * extra_scale + value_min_exact = lo_exact if lo_exact < hi_exact else hi_exact + value_max_exact = hi_exact if lo_exact < hi_exact else lo_exact + + # A final raw-output transition k-1 -> k occurs at (k-1/2)*eval_guard. + # Include exactly every k whose guarded uncertainty band + # [(k-1/2)G-d/2, (k-1/2)G+d/2] intersects the polynomial endpoint range. + # These floor/ceil operations are FLINT exact rationals; binary floats are + # used only later to propose a root center whose bracket is exact-checked. + first_k = int( + ((2 * value_min_exact - degree + eval_guard) / (2 * eval_guard)).ceil() + ) + last_k = int( + ((2 * value_max_exact + degree + eval_guard) / (2 * eval_guard)).floor() + ) + ordered_k = range(first_k, last_k + 1) + if piece.tail: + ordered_k = range(last_k, first_k - 1, -1) + + min_guard_per_raw = ( + float(certificate.derivative_min_guard) * (RECIPROCAL // Q) / Q + ) + base_radius = max(16, math.ceil((degree + 2) / min_guard_per_raw) + 16) + intervals: list[tuple[int, int, int]] = [] + t_guess = -1.0 + total_thresholds = last_k - first_k + 1 + print( + f"discrete-brackets {piece.name}: candidate threshold span={total_thresholds}", + flush=True, + ) + + for sequence, k in enumerate(ordered_k): + threshold_twice = (2 * k - 1) * eval_guard + low_twice = threshold_twice - degree + high_twice = threshold_twice + degree + + # Skip threshold bands disjoint from the exact polynomial endpoint range. + left_cmp_low = (2 * lo_exact > low_twice) - (2 * lo_exact < low_twice) + left_cmp_high = (2 * lo_exact > high_twice) - (2 * lo_exact < high_twice) + right_cmp_low = (2 * hi_exact > low_twice) - (2 * hi_exact < low_twice) + right_cmp_high = (2 * hi_exact > high_twice) - (2 * hi_exact < high_twice) + if not piece.tail: + if right_cmp_low < 0 or left_cmp_high > 0: + continue + else: + if left_cmp_low < 0 or right_cmp_high > 0: + continue + + target_raw = k - 0.5 + # Floating point only proposes a center. Exact inequalities below make + # the resulting bracket independent of floating-point accuracy. + for _ in range(4): + value, derivative = float_poly(piece, t_guess) + if derivative == 0: + break + t_guess -= (value - target_raw) / derivative + t_guess = min(1.0, max(-1.0, t_guess)) + center = round(piece.mid_raw + HW_RAW * t_guess) + radius = base_radius + + while True: + left = max(lo, center - radius) + right = min(hi, center + radius) + if not piece.tail: + left_ok = left == lo or compare_poly_to_twice_guarded( + piece, left, low_twice + ) < 0 + right_ok = right == hi or compare_poly_to_twice_guarded( + piece, right, high_twice + ) > 0 + else: + left_ok = left == lo or compare_poly_to_twice_guarded( + piece, left, high_twice + ) > 0 + right_ok = right == hi or compare_poly_to_twice_guarded( + piece, right, low_twice + ) < 0 + if left_ok and right_ok: + break + radius *= 2 + assert radius <= hi - lo + 1 + + intervals.append((piece_index, left, right)) + if sequence and sequence % 1_000_000 == 0: + print( + f" {piece.name}: {sequence}/{total_thresholds} thresholds", + flush=True, + ) + + # Adjacent/overlapping bands can be merged without weakening exhaustion. + merged: list[tuple[int, int, int]] = [] + for record in sorted(intervals, key=lambda item: item[1]): + if merged and record[1] <= merged[-1][2] + 1: + old = merged[-1] + merged[-1] = (piece_index, old[1], max(old[2], record[2])) + else: + merged.append(record) + checked_pairs = sum(right - left for _, left, right in merged) + print( + f" {piece.name}: records={len(merged)} exact-pairs={checked_pairs}", + flush=True, + ) + return merged, checked_pairs + + +def rust_array(values: tuple[int, ...]) -> str: + padded = list(values) + [0] * (9 - len(values)) + return "[" + ",".join(str(value) for value in padded) + "]" + + +def render_rust_verifier(pieces: list[Piece]) -> str: + coefficients = ",\n".join(rust_array(piece.coefficients) for piece in pieces) + lengths = ",".join(str(len(piece.coefficients)) for piece in pieces) + mids = ",".join(str(piece.mid_raw) for piece in pieces) + tails = ",".join("true" if piece.tail else "false" for piece in pieces) + return f""" +use std::env; +use std::fs; + +const Q: i128 = 1i128 << 44; +const GUARD: i128 = {GUARD}; +const TAIL_EXTRA_Q: u32 = {TAIL_EXTRA_Q}; +const RECIP: i128 = {RECIPROCAL}; +const COEFFS: [[i64; 9]; {len(pieces)}] = [ +{coefficients} +]; +const LENGTHS: [usize; {len(pieces)}] = [{lengths}]; +const MIDS: [i128; {len(pieces)}] = [{mids}]; +const TAIL: [bool; {len(pieces)}] = [{tails}]; + +fn rn(value: i128, divisor: i128) -> i128 {{ + if value >= 0 {{ (value + divisor / 2) / divisor }} + else {{ -((-value + divisor / 2) / divisor) }} +}} + +fn value(piece: usize, x: i128) -> i128 {{ + let t = rn((x - MIDS[piece]) * RECIP, Q); + let n = LENGTHS[piece]; + let extra_q = if TAIL[piece] {{ TAIL_EXTRA_Q }} else {{ 0 }}; + let mut result = COEFFS[piece][n - 1] << extra_q; + for index in (0..n - 1).rev() {{ + result = rn(result as i128 * t, Q) as i64 + (COEFFS[piece][index] << extra_q); + }} + rn(result as i128, GUARD << extra_q) +}} + +fn get_i64(bytes: &[u8]) -> i64 {{ + i64::from_le_bytes(bytes.try_into().unwrap()) +}} + +fn main() {{ + let path = env::args().nth(1).expect("candidate file"); + let bytes = fs::read(path).unwrap(); + assert_eq!(bytes.len() % 24, 0); + let mut pairs: u128 = 0; + let mut records: u64 = 0; + for record in bytes.chunks_exact(24) {{ + let piece = record[0] as usize; + let left = get_i64(&record[8..16]) as i128; + let right = get_i64(&record[16..24]) as i128; + let mut previous = value(piece, left); + let mut x = left + 1; + while x <= right {{ + let current = value(piece, x); + if (!TAIL[piece] && current < previous) || (TAIL[piece] && current > previous) {{ + panic!("monotonicity failure piece={{}} x={{}} prev={{}} current={{}}", piece, x, previous, current); + }} + previous = current; + x += 1; + pairs += 1; + }} + records += 1; + }} + println!("records={{records}} pairs={{pairs}}"); +}} +""" + + +def run_ambiguous_cell_verifier( + pieces: list[Piece], certificates: list[PieceCertificate] +) -> tuple[int, int, str]: + all_intervals: list[tuple[int, int, int]] = [] + expected_pairs = 0 + for index, certificate in enumerate(certificates): + if certificate.monotone_by_margin: + continue + intervals, pair_count = candidate_intervals(index, certificate) + all_intervals.extend(intervals) + expected_pairs += pair_count + + with tempfile.TemporaryDirectory(prefix="solmath-cdf-proof-") as directory: + directory_path = Path(directory) + records_path = directory_path / "ambiguous.bin" + verifier_path = directory_path / "verify.rs" + binary_path = directory_path / "verify" + with records_path.open("wb") as output: + for piece, left, right in all_intervals: + output.write(struct.pack(" int: + if raw_x == 0: + return SCALE // 2 + if raw_x <= 5 * SCALE: + index = min((raw_x - 1) // (SCALE // 2), 9) + return rounded_piece_value(pieces[index], raw_x) + if raw_x <= 7 * SCALE: + index = 10 + min((raw_x - 5 * SCALE - 1) // (SCALE // 2), 3) + tail = rounded_piece_value(pieces[index], raw_x) + return SCALE - max(0, min(SCALE, tail)) + if raw_x <= TAIL_CUTOFF: + return SCALE - 1 + return SCALE + + +def certify_seams_and_cutoff( + pieces: list[Piece], +) -> tuple[list[tuple[int, int, int]], arb, arb, arb, arb]: + # Include the exact x=0 special return and its first positive raw neighbor, + # then every dispatch seam and the half-raw tail transition. + seams = [0] + [i * SCALE // 2 for i in range(1, 15)] + [TAIL_CUTOFF] + seam_values: list[tuple[int, int, int]] = [] + for seam in seams: + at = positive_kernel_value(pieces, seam) + after = positive_kernel_value(pieces, seam + 1) + assert after >= at, f"nonmonotone seam at raw {seam}" + seam_values.append((seam, at, after)) + + sqrt_two = arb(2).sqrt() + tail_at_seven = arb(SCALE) * (arb(7) / sqrt_two).erfc() / 2 + cutoff_x = arb(TAIL_CUTOFF) / SCALE + cutoff_tail = arb(SCALE) * (cutoff_x / sqrt_two).erfc() / 2 + after_x = arb(TAIL_CUTOFF + 1) / SCALE + after_tail = arb(SCALE) * (after_x / sqrt_two).erfc() / 2 + tail_at_eight = arb(SCALE) * (arb(8) / sqrt_two).erfc() / 2 + assert tail_at_seven < arb("1.5") + assert cutoff_tail > arb("0.5") + assert after_tail < arb("0.5") + assert tail_at_eight < arb("0.001") + assert 7 * SCALE < TAIL_CUTOFF < 8 * SCALE + return seam_values, tail_at_seven, cutoff_tail, after_tail, tail_at_eight + + +def fmt(value: arb, digits: int = 12) -> str: + return value.str(digits, radius=False) + + +def sha256_text(text: str) -> str: + return hashlib.sha256(text.encode()).hexdigest() + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--skip-discrete", + action="store_true", + help="run continuous accuracy proof only (not a release certificate)", + ) + args = parser.parse_args() + assert flint.__version__ == "0.8.0", flint.__version__ + ctx.prec = ARB_BITS + started = time.monotonic() + pieces, coefficient_text, kernel_text, modeled_kernel = parse_sources() + certify_integer_safety(pieces) + + reciprocal_residual = abs(RECIPROCAL * HW_RAW - Q * Q) + assert 2 * reciprocal_residual <= HW_RAW + assert map_t(0, HW_RAW) == -Q + assert map_t(2 * HW_RAW, HW_RAW) == Q + + certificates: list[PieceCertificate] = [] + print("piece approximation map integer total derivative-margin") + for piece in pieces: + certificate = certify_piece(piece) + certificates.append(certificate) + assert certificate.total_error < arb("2.5") + print( + piece.name, + fmt(certificate.approximation), + fmt(certificate.map_error), + fmt(certificate.integer_error), + fmt(certificate.total_error), + "analytic" if certificate.monotone_by_margin else "cell-audit", + ) + + seam_values, tail_at_seven, cutoff_tail, after_tail, tail_at_eight = ( + certify_seams_and_cutoff(pieces) + ) + if args.skip_discrete: + records = pairs = 0 + discrete = "SKIPPED" + else: + records, pairs, discrete = run_ambiguous_cell_verifier(pieces, certificates) + + maximum = arb(0) + worst = "" + for certificate in certificates: + if not certificate.total_error < maximum: + maximum = maximum.max(certificate.total_error) + worst = certificate.piece.name + assert maximum < arb("2.5") + elapsed = time.monotonic() - started + print(f"worst={worst} bound={fmt(maximum, 16)}") + print( + "tail_at_7=" + fmt(tail_at_seven, 16), + "cutoff_tail=" + fmt(cutoff_tail, 16), + "after_cutoff_tail=" + fmt(after_tail, 16), + "tail_at_8=" + fmt(tail_at_eight, 16), + ) + print(f"seams={len(seam_values)} discrete={discrete}") + print(f"coefficient_sha256={sha256_text(coefficient_text)}") + print(f"modeled_kernel_sha256={sha256_text(modeled_kernel)}") + print(f"kernel_sha256={sha256_text(kernel_text)}") + print(f"python_flint={flint.__version__} arb_bits={ARB_BITS} elapsed={elapsed:.1f}s") + if not args.skip_discrete: + print( + "CERTIFIED: real error < 2.5 raw units; nearest-integer reference " + "error <= 2 ULP; exact symmetry and all-i128 discrete monotonicity" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/crosscheck_quantlib.py b/scripts/crosscheck_quantlib.py index 3242404..a247ffb 100644 --- a/scripts/crosscheck_quantlib.py +++ b/scripts/crosscheck_quantlib.py @@ -191,7 +191,15 @@ def main(): sm_vs_mp[g].append(sig_figs_agreement(sm_v, mp_v)) if errors: - print(f" ({errors} vectors skipped due to errors)\n") + raise RuntimeError( + f"cross-validation failed closed: {errors}/{len(samples)} vectors " + "could not be compared" + ) + if not samples: + raise RuntimeError("cross-validation selected zero samples") + empty = [g for g in GREEKS if not ql_vs_mp[g] or not sm_vs_ql[g] or not sm_vs_mp[g]] + if empty: + raise RuntimeError(f"cross-validation produced no comparisons for: {empty}") # Compute statistics def stats(vals): diff --git a/scripts/fit_american_kbi_price_weights.py b/scripts/fit_american_kbi_price_weights.py new file mode 100644 index 0000000..1a08f09 --- /dev/null +++ b/scripts/fit_american_kbi_price_weights.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +"""Reproduce KBI's fixed nine-node QdFp empirical cubature weights. + +The fit changes only global integration weights. Runtime nodes, boundary +reconstruction, normal kernels, and all six quote inputs remain live on-chain. +No option price or contract-specific coefficient is embedded in the program. +""" + +from __future__ import annotations + +import argparse +import json +import math + +import numpy as np +from scipy.optimize import lsq_linear +from scipy.special import ndtr + +import american_quantlib_reference as base +from generate_american_kbi_data import KBI_QDFP_PRICE_WEIGHTS +from american_kbi_reference import SmoothPastingKbi, european_put + + +POWER = 2.25 +ORDER = 9 +RIDGE = 1.0e-3 + + +def boundary_at(samples: np.ndarray, boundary) -> np.ndarray: + left = np.searchsorted(boundary.times, samples, side="right") - 1 + left = np.clip(left, 0, boundary.times.size - 2) + fraction = ( + (samples - boundary.times[left]) + / (boundary.times[left + 1] - boundary.times[left]) + ) + return np.exp( + np.log(boundary.values[left]) + + fraction + * (np.log(boundary.values[left + 1]) - np.log(boundary.values[left])) + ) + + +def premium_row( + spot: float, + boundary, + rate: float, + yield_rate: float, + sigma: float, + y: np.ndarray, +) -> np.ndarray: + maturity = float(boundary.times[-1]) + lag = maturity * y**POWER + boundary_samples = boundary_at(maturity - lag, boundary) + standard_deviation = sigma * np.sqrt(lag) + d1 = ( + np.log(spot / boundary_samples) + + (rate - yield_rate + 0.5 * sigma * sigma) * lag + ) / standard_deviation + d2 = d1 - standard_deviation + return ( + rate * maturity * np.exp(-rate * lag) * ndtr(-d2) + - yield_rate + * maturity + * spot + * np.exp(-yield_rate * lag) + * ndtr(-d1) + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + + contracts = base.contracts(0xA3E1, 48) + if len(contracts) != 48: + raise RuntimeError(f"expected 48 training contracts, found {len(contracts)}") + + method = SmoothPastingKbi( + nodes=18, + quadrature_order=2, + grading=2.0, + product_basis="gauss-y", + root_solver="newton", + newton_steps=2, + price_mesh="global", + price_power=POWER, + late_newton_steps=1, + newton_cutover=2, + derivative_mode="full", + price_boundary_interp="log", + boundary_normal="exact", + price_normal="exact", + boundary_order=6, + price_order=ORDER, + third_predictor_alpha=0.925, + ) + gauss_x, gauss_weight = np.polynomial.legendre.leggauss(ORDER) + y = 0.5 * (gauss_x + 1.0) + gaussian_weights = ( + POWER * y ** (POWER - 1.0) * 0.5 * gauss_weight + ) + spots = np.exp(np.linspace(-0.75, 0.75, 33)) + qdfp = base.QdFpSurfacePricer() + rows: list[np.ndarray] = [] + targets: list[float] = [] + + for contract in contracts: + maturity = contract.days / 365.0 + put_boundary = method.boundary( + maturity, contract.r, contract.q, contract.sigma + ) + call_boundary = method.boundary( + maturity, contract.q, contract.r, contract.sigma + ) + put_truth = qdfp.surface(spots, contract, contract.days, False) + call_truth = qdfp.surface(spots, contract, contract.days, True) + + # Exact call-put duality maps the call to a put with spot 1/S. Scale + # each dual residual by S so least squares minimizes original call + # dollars rather than dual normalized dollars. + legs = ( + ( + put_boundary, + contract.r, + contract.q, + spots, + put_truth, + np.ones_like(spots), + ), + ( + call_boundary, + contract.q, + contract.r, + 1.0 / spots, + call_truth / spots, + spots, + ), + ) + for boundary, rate, yield_rate, leg_spots, truth, scale in legs: + for spot, exact, row_scale in zip(leg_spots, truth, scale): + if spot <= boundary.values[-1]: + continue + european = european_put( + float(spot), maturity, rate, yield_rate, contract.sigma + ) + rows.append( + float(row_scale) + * premium_row( + float(spot), + boundary, + rate, + yield_rate, + contract.sigma, + y, + ) + ) + targets.append(float(row_scale) * (float(exact) - european)) + + matrix = np.asarray(rows, dtype=np.float64) + target = np.asarray(targets, dtype=np.float64) + root_ridge = math.sqrt(RIDGE) + augmented_matrix = np.vstack( + ( + matrix, + root_ridge * np.eye(ORDER), + 100.0 * np.ones((1, ORDER)), + ) + ) + augmented_target = np.concatenate( + (target, root_ridge * gaussian_weights, np.asarray([100.0])) + ) + result = lsq_linear( + augmented_matrix, + augmented_target, + bounds=(0.0, np.inf), + tol=1.0e-14, + lsmr_tol=1.0e-14, + max_iter=2_000, + ) + weights = result.x / np.sum(result.x) + maximum_difference = float(np.max(np.abs(weights - KBI_QDFP_PRICE_WEIGHTS))) + print(json.dumps({ + "training_contracts": len(contracts), + "training_rows": int(matrix.shape[0]), + "ridge": RIDGE, + "weights": weights.tolist(), + "maximum_committed_difference": maximum_difference, + }, indent=2)) + if args.check and maximum_difference > 5.0e-15: + raise SystemExit("committed KBI empirical weights do not reproduce") + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_adversarial_vectors.py b/scripts/generate_adversarial_vectors.py index 165d1ec..1651586 100644 --- a/scripts/generate_adversarial_vectors.py +++ b/scripts/generate_adversarial_vectors.py @@ -5,7 +5,7 @@ Reference values from mpmath at 60-digit precision. """ -import json, os, random +import json, os, random, sys import mpmath mpmath.mp.dps = 60 @@ -47,8 +47,7 @@ def pad_to(vecs, target, gen_one): def gen_ln(): print("ln_fixed_i...") vecs = [] - TABLE_STEP = SCALE // 16 - TABLE_HALF_STEP = SCALE // 32 + lut_step = SCALE // 1024 def ln_vec(x, cat): if x <= 0 or x > U128_MAX: return None @@ -56,44 +55,213 @@ def ln_vec(x, cat): if abs(ref) > I128_MAX: return None return {"x": s(x), "expected": s(ref), "category": cat} - # Zone 1: near x=1.0 (2500) - pad_to(vecs, 2500, lambda: ln_vec(int((1.0 + random.uniform(-0.001, 0.001)) * SCALE), "near_1")) + # Every raw increment around one plus both sides of the correctly-rounded + # direct-return seam at |x-1| = 1e-6. + for delta in range(-1_000, 1_001): + vecs.append(ln_vec(SCALE + delta, "near_one_raw")) + for sign in (-1, 1): + for offset in range(-128, 129): + vecs.append(ln_vec(SCALE + sign * (1_000_000 + offset), "near_one_seam")) + + # Cross every normalized midpoint-table boundary at k=0. + for j in range(1025): + boundary = SCALE + j * lut_step + side = -1 if j % 2 == 0 else 1 + for x in (boundary, boundary + side): + v = ln_vec(x, "lut_table_boundary") + if v is not None: + vecs.append(v) + + # Exact powers of two and neighboring raw values across the public range. + for k in range(-40, 89): + base = SCALE << k if k >= 0 else SCALE >> -k + for offset in (0, -1, 1, -2, 2): + v = ln_vec(base + offset, "power_of_two") + if v is not None: + vecs.append(v) + + # Mantissa interiors across every reachable exponent, including k=88 for + # the upper half of the u128 domain. + offsets = ( + -488_281_249, -366_210_937, -244_140_625, -122_070_312, + -1, 0, 1, 122_070_312, 244_140_625, 366_210_937, 488_281_249, + ) + for k in range(-40, 89): + for lane in range(36): + j = (73 * k + 29 * lane) % 1024 + midpoint = SCALE + j * lut_step + lut_step // 2 + mantissa = midpoint + offsets[lane % len(offsets)] + x = mantissa << k if k >= 0 else mantissa >> -k + v = ln_vec(x, "reduction_k_sweep") + if v is not None: + vecs.append(v) + + for x in (1, 2, U128_MAX - 1, U128_MAX): + vecs.append(ln_vec(x, "domain_extreme")) + + pad_to(vecs, 10000, lambda: ln_vec( + random.randint(1, U128_MAX), "full_width_fill")) + vecs = [v for v in vecs if v is not None][:10000] - # Zone 2: table boundaries (fixed ~204, deterministic) - for j in range(17): - boundary = SCALE + j * TABLE_STEP - for offset in [0, 1, -1, 2, -2, 10, -10, 100, -100, TABLE_HALF_STEP - 1, TABLE_HALF_STEP, TABLE_HALF_STEP + 1]: - v = ln_vec(boundary + offset, f"table_boundary_j{j}") - if v: vecs.append(v) + save("adv_ln_vectors.json", + {"suite": "adversarial", "function": "ln_fixed_i", + "zones": "near-one raw/seam, every LUT boundary, powers of two, all reachable exponents, u128 extremes, full-width fill", + "reference": "mpmath.log at 60 decimal digits"}, vecs) - # Zone 3: direct/table seam (101, deterministic) - for off in range(-50, 51): - v = ln_vec(SCALE + TABLE_HALF_STEP + off, "direct_table_seam") - if v: vecs.append(v) - # Zone 4: powers of 2 (~490, deterministic) - for k in range(-30, 40): - base = int(mpmath.power(2, k) * SCALE) - for off in [0, 1, -1, 10, -10, 1000, -1000]: - v = ln_vec(base + off, f"pow2_k{k}") - if v: vecs.append(v) +# ============================================================ +# LN_1P — 10,000 vectors +# ============================================================ - # Zone 5: very small (fill to 6000) - pad_to(vecs, 6000, lambda: ln_vec(int(10 ** random.uniform(-12, -3) * SCALE), "very_small")) +def gen_ln_1p(): + print("ln_1p_fixed...") + vecs = [] - # Zone 6: very large (fill to 8000) - pad_to(vecs, 8000, lambda: ln_vec(int(10 ** random.uniform(3, 15) * SCALE), "very_large")) + def ln_1p_vec(x, cat): + if x <= -SCALE or x > I128_MAX: + return None + ref = nint(mpmath.log1p(mpmath.mpf(x) / SCALE) * SCALE) + return {"x": s(x), "expected": s(ref), "category": cat} - # Zone 7: dense sweep (fill to 10000) - for i in range(10000 - len(vecs)): - x = int((1.0 + i / (10000 - len(vecs) + 1)) * SCALE) - v = ln_vec(x, "dense_sweep") - if v: vecs.append(v) - pad_to(vecs, 10000, lambda: ln_vec(int(random.uniform(0.5, 3.0) * SCALE), "fill")) + # Domain edge: make 1+x range from one raw unit to 1e-2. + for one_plus_x in range(1, 257): + vecs.append(ln_1p_vec(-SCALE + one_plus_x, "domain_edge_raw")) + pad_to(vecs, 768, lambda: ln_1p_vec( + -SCALE + max(1, int(10 ** random.uniform(0, 10))), "domain_edge_log")) + + # Every raw increment in the central proof interval sample. + for x in range(-1_000, 1_001): + vecs.append(ln_1p_vec(x, "near_zero_raw")) + + # Exercise both sides of the direct-return seam at |x| = 1e-6. + for sign in (-1, 1): + for offset in range(-128, 129): + vecs.append(ln_1p_vec(sign * (1_000_000 + offset), "near_zero_seam")) + + # Cross every table boundary at k=0. For each boundary include the exact + # value and alternate one adjacent raw unit. + LUT_STEP = SCALE // 1024 + for j in range(1025): + side = -1 if j % 2 == 0 else 1 + normalized = SCALE + j * LUT_STEP + for one_plus_x in (normalized, normalized + side): + vecs.append(ln_1p_vec(one_plus_x - SCALE, "lut_table_boundary")) + + # Permanent regressions discovered by the production corpus. These are + # reduction/rounding combinations that attained its current 2-ULP maximum. + for x in ( + -999_999_993_148, + -997_601_045_585, + 4_477_046_948_564_625, + 1_252_655_015_786_461, + 4_255_737_139_032_047, + 1_211_973_668_939_694, + 5_811_452_000_770_234, + 2_065_446_030_829_052, + ): + vecs.append(ln_1p_vec(x, "production_worst_regression")) + + # Exercise mantissa interiors across every reachable binary exponent. + # This targets the interaction between local rounding and k*ln(2), which + # the former two-octave corpus missed. + offsets = ( + -488_281_249, -366_210_937, -244_140_625, -122_070_312, + -1, 0, 1, 122_070_312, 244_140_625, 366_210_937, 488_281_249, + ) + for k in range(-40, 88): + for lane in range(36): + j = (73 * k + 29 * lane) % 1024 + midpoint = SCALE + j * LUT_STEP + LUT_STEP // 2 + mantissa = midpoint + offsets[lane % len(offsets)] + one_plus_x = mantissa << k if k >= 0 else mantissa >> -k + v = ln_1p_vec(one_plus_x - SCALE, "reduction_k_sweep") + if v is not None: + vecs.append(v) + + pad_to(vecs, 10000, lambda: ln_1p_vec( + random.randint(-900_000_000_000, 10 * SCALE), "mixed_fill")) + + vecs = [v for v in vecs if v is not None][:10000] + save("adv_ln_1p_vectors.json", + {"suite": "adversarial", "function": "ln_1p_fixed", + "zones": "domain edge, near-zero raw units, fast-path seams, every LUT boundary, all reachable binary exponents, production-worst regressions, signed fill", + "reference": "mpmath.log1p at 60 decimal digits"}, vecs) - save("adv_ln_vectors.json", - {"suite": "adversarial", "function": "ln_fixed_i", - "zones": "near_1, table_boundaries, direct_table_seam, pow2, very_small, very_large, dense_sweep"}, vecs) + +# ============================================================ +# EXPM1 — 10,000 vectors +# ============================================================ + +def gen_expm1(): + print("expm1_fixed...") + vecs = [] + def expm1_vec(x, cat): + if x >= 40 * SCALE or x < I128_MIN: + return None + ref = nint(mpmath.expm1(mpmath.mpf(x) / SCALE) * SCALE) + if ref < I128_MIN or ref > I128_MAX: + return None + return {"x": s(x), "expected": s(ref), "category": cat} + + # Exact raw-unit neighborhood and both sides of the direct-return seam. + for x in range(-1_000, 1_001): + vecs.append(expm1_vec(x, "near_zero_raw")) + for sign in (-1, 1): + for offset in range(-128, 129): + vecs.append(expm1_vec(sign * (1_000_000 + offset), "near_zero_seam")) + + # Half-ln2 reduction seams for every reachable exponent. + for k in range(-58, 59): + center = nint((mpmath.mpf(k) + mpmath.mpf("0.5")) * mpmath.log(2) * SCALE) + for offset in (-2, -1, 0, 1, 2): + v = expm1_vec(center + offset, "ln2_reduction_seam") + if v is not None: + vecs.append(v) + + # Every power-of-two LUT boundary at k=0, exact and one adjacent raw unit. + r_min = -346_573_590_280 + step = 1 << 29 + half_ln2 = 346_573_590_280 + for j in range(1293): + boundary = r_min + j * step + if boundary > half_ln2: + break + side = -1 if j % 2 == 0 else 1 + vecs.append(expm1_vec(boundary, "lut_boundary")) + vecs.append(expm1_vec(boundary + side, "lut_boundary")) + + # Mantissa interiors across the complete k range. + offsets = (-268_435_455, -201_326_592, -134_217_728, -67_108_864, + -1, 0, 1, 67_108_864, 134_217_728, 201_326_592, 268_435_455) + for k in range(-58, 59): + k_ln2 = nint(mpmath.mpf(k) * mpmath.log(2) * SCALE) + for lane in range(32): + j = (71 * k + 31 * lane) % 1292 + midpoint = r_min + j * step + step // 2 + x = k_ln2 + midpoint + offsets[lane % len(offsets)] + v = expm1_vec(x, "reduction_k_sweep") + if v is not None: + vecs.append(v) + + # Public saturation/overflow seams; overflow inputs are excluded because + # this corpus measures successful numerical results. + for center in (-40 * SCALE, 40 * SCALE): + for offset in range(-20, 21): + v = expm1_vec(center + offset, "domain_limit_seam") + if v is not None: + vecs.append(v) + + # Permanent regression attaining the production corpus's full-domain + # absolute-error maximum on the retained implementation. + vecs.append(expm1_vec(39_981_510_191_812, "production_worst_regression")) + + pad_to(vecs, 10000, lambda: expm1_vec( + random.randint(-40 * SCALE, 40 * SCALE - 1), "mixed_fill")) + vecs = [v for v in vecs if v is not None][:10000] + save("adv_expm1_vectors.json", + {"suite": "adversarial", "function": "expm1_fixed", + "zones": "near-zero raw/seam, all ln2 reduction seams, every LUT boundary, all reachable exponents, domain limits, signed fill", + "reference": "mpmath.expm1 at 60 decimal digits"}, vecs) # ============================================================ # EXP — 10,000 vectors @@ -103,43 +271,78 @@ def gen_exp(): print("exp_fixed_i...") vecs = [] LN2 = mpmath.log(2) + phases = 32 def exp_vec(x, cat): + if x <= -40 * SCALE: + return {"x": s(x), "expected": "0", "category": cat} + if x >= 40 * SCALE: + return None ref = nint(mpmath.exp(mpmath.mpf(x) / SCALE) * SCALE) - if ref > U128_MAX or ref <= 0: return None + if ref > I128_MAX or ref < 0: return None return {"x": s(x), "expected": s(ref), "category": cat} - # Zone 1: ln2 multiples (~880, deterministic) - for k in range(-40, 40): - x_scaled = int(float(k * LN2) * SCALE) - for off in [0, 1, -1, 2, -2, 5, -5, 50, -50, 500, -500]: - v = exp_vec(x_scaled + off, f"ln2_k{k}") - if v: vecs.append(v) - - # Zone 2: near zero (fill to 3500) - pad_to(vecs, 3500, lambda: exp_vec(int(random.uniform(-0.001, 0.001) * SCALE), "near_zero")) - - # Zone 3: large negative (fill to 5500) - pad_to(vecs, 5500, lambda: exp_vec(int(random.uniform(-25, -5) * SCALE), "large_negative")) - - # Zone 4: moderate positive (fill to 7500) - pad_to(vecs, 7500, lambda: exp_vec(int(random.uniform(0.01, 2.0) * SCALE), "moderate_positive")) + # Bracket every nearest-cell decision seam in the full successful domain. + # floor/ceil are the two adjacent raw inputs surrounding the exact + # half-cell boundary, so the adversarial suite cannot miss a phase-table + # discontinuity or hide the amplified positive-tail worst case. + step = LN2 / phases + for cell in range(-2_000, 2_001): + boundary = (mpmath.mpf(cell) + mpmath.mpf("0.5")) * step * SCALE + if not (-40 * SCALE < boundary < 40 * SCALE): + continue + lower = int(mpmath.floor(boundary)) + upper = int(mpmath.ceil(boundary)) + vecs.append(exp_vec(lower, "cell_seam")) + vecs.append(exp_vec(upper, "cell_seam")) + + # Sample cell interiors across every phase and reachable octave. + for cell in range(-1_840, 1_841, 4): + x = nint(mpmath.mpf(cell) * step * SCALE) + v = exp_vec(x, "cell_interior") + if v is not None: + vecs.append(v) - # Zone 5: near overflow (fill to 9000) - pad_to(vecs, 9000, lambda: exp_vec(int(random.uniform(25.0, 38.5) * SCALE), "near_overflow")) + # Exact octave points and adjacent raw inputs retain the previous ln2 + # regressions while the cell sweep above targets the new implementation. + for k in range(-58, 58): + x_scaled = nint(mpmath.mpf(k) * LN2 * SCALE) + for offset in (-1, 0, 1): + v = exp_vec(x_scaled + offset, "ln2_multiple") + if v is not None: + vecs.append(v) + + # Correctly-rounded direct-return seams and raw values around zero. + for sign in (-1, 1): + for offset in range(-8, 9): + vecs.append(exp_vec(sign * 1_000_000 + offset, "tiny_direct_seam")) + for x in range(-2_048, 2_049, 17): + vecs.append(exp_vec(x, "near_zero_raw")) + + # Saturation/overflow guards and a dense successful strip immediately + # below +40, where power-of-two reconstruction amplifies absolute error. + for offset in range(-16, 17): + v = exp_vec(-40 * SCALE + offset, "negative_domain_seam") + if v is not None: + vecs.append(v) + v = exp_vec(40 * SCALE + offset, "positive_domain_seam") + if v is not None: + vecs.append(v) + for offset in range(1, 257): + vecs.append(exp_vec(40 * SCALE - offset, "positive_limit_raw")) - # Zone 6: reduction midpoints (deterministic, ~120) - for k in range(-20, 20): - for frac in [-0.25, 0.0, 0.25]: - v = exp_vec(int(float(k * LN2 + frac * LN2) * SCALE), "reduction_midpoint") - if v: vecs.append(v) + # Permanent regressions from the old and phased kernels. + for x in (19_998_424_170_953, 38_468_881_341_913, 39_996_758_403_249): + vecs.append(exp_vec(x, "worst_regression")) - # Fill remainder - pad_to(vecs, 10000, lambda: exp_vec(int(random.uniform(-20, 20) * SCALE), "fill")) + pad_to(vecs, 10000, lambda: exp_vec( + random.randint(-40 * SCALE, 40 * SCALE - 1), "full_domain_fill")) + vecs = [v for v in vecs if v is not None][:10000] save("adv_exp_vectors.json", {"suite": "adversarial", "function": "exp_fixed_i", - "zones": "ln2_multiples, near_zero, large_negative, moderate_positive, near_overflow, reduction_midpoints"}, vecs) + "zones": "all ln2/32 cell seams, phased interiors, ln2 multiples, tiny-direct seam, raw zero, domain guards, dense +40 tail, permanent worst points, full-domain fill", + "reference": "mpmath.exp at 60 decimal digits; contract saturation at x <= -40"}, vecs) # ============================================================ # NORM_CDF — 10,000 vectors @@ -148,7 +351,15 @@ def exp_vec(x, cat): def gen_norm_cdf(): print("norm_cdf_poly...") vecs = [] - boundaries = [0.5, 1.5, 3.0, 5.0] + rng = random.Random(0xCDF2026) + cutoff = 7_130_506_848_171 + boundaries = [ + SCALE // 2, SCALE, 3 * SCALE // 2, 2 * SCALE, + 5 * SCALE // 2, 3 * SCALE, 7 * SCALE // 2, 4 * SCALE, + 9 * SCALE // 2, 5 * SCALE, 11 * SCALE // 2, 6 * SCALE, + 13 * SCALE // 2, 7 * SCALE, + cutoff, 8 * SCALE, + ] def cdf_ref(x_scaled): x_mp = mpmath.mpf(x_scaled) / SCALE @@ -157,45 +368,77 @@ def cdf_ref(x_scaled): def cdf_vec(x, cat): return {"x": s(x), "expected": s(cdf_ref(x)), "category": cat} - # Zone 1: boundaries (~608, deterministic) - for b in boundaries: - b_scaled = int(b * SCALE) - for off in [0, 1, -1, 2, -2, 5, -5, 10, -10, 50, -50, 100, -100, 500, -500, 1000, -1000, 5000, -5000]: + # Every body/tail/saturation seam, on both signs, with dense raw offsets. + raw_offsets = [0] + for off in (1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, + 377, 610, 987, 1_597, 2_584, 4_096): + raw_offsets.extend((off, -off)) + for boundary in boundaries: + for off in raw_offsets: for sign in [1, -1]: - vecs.append(cdf_vec(sign * (b_scaled + off), f"boundary_{b}")) - - # Zone 2: piece interiors (fill to 7000) - pieces = [(0, 0.5), (0.5, 1.5), (1.5, 3.0), (3.0, 5.0), (5.0, 8.5)] - per_piece = (7000 - len(vecs)) // (len(pieces) * 2) - for (lo, hi) in pieces: - for _ in range(per_piece): - x = int(random.uniform(lo, hi) * SCALE) - vecs.append(cdf_vec(x, f"interior_{lo}_{hi}")) - vecs.append(cdf_vec(-x, f"interior_neg_{lo}_{hi}")) - - # Zone 3: deep tails (fill to 9000) - while len(vecs) < 9000: - x = int(random.uniform(5.0, 8.5) * SCALE) - vecs.append(cdf_vec(x, "deep_right_tail")) - vecs.append(cdf_vec(-x, "deep_left_tail")) - - # Zone 4: zero + extreme tails + vecs.append(cdf_vec(sign * (boundary + off), + f"seam_{boundary}")) + + # Raw values around zero exercise exact symmetry and the center branch. + for x in range(-4_096, 4_097, 17): + vecs.append(cdf_vec(x, "near_zero_raw")) + + # Interior coverage for every actual polynomial and exact-tail interval. + pieces = [ + (0, SCALE // 2), (SCALE // 2, SCALE), + (SCALE, 3 * SCALE // 2), (3 * SCALE // 2, 2 * SCALE), + (2 * SCALE, 5 * SCALE // 2), (5 * SCALE // 2, 3 * SCALE), + (3 * SCALE, 7 * SCALE // 2), (7 * SCALE // 2, 4 * SCALE), + (4 * SCALE, 9 * SCALE // 2), (9 * SCALE // 2, 5 * SCALE), + (5 * SCALE, 11 * SCALE // 2), (11 * SCALE // 2, 6 * SCALE), + (6 * SCALE, 13 * SCALE // 2), (13 * SCALE // 2, 7 * SCALE), + (7 * SCALE, cutoff), (cutoff, 8 * SCALE), + ] + lane = 0 + while len(vecs) < 7_800: + lo, hi = pieces[lane % len(pieces)] + x = rng.randint(lo, hi) + sign = 1 if (lane // len(pieces)) % 2 == 0 else -1 + vecs.append(cdf_vec(sign * x, f"interior_{lo}_{hi}")) + lane += 1 + + # Hit true-reference half-ULP transitions throughout the direct tail. + # These are stronger than uniform random tails because a one-raw-input + # movement can change the correctly rounded output. + for index in range(600): + exponent = mpmath.mpf(index) / 599 + tail_raw = mpmath.power(10, exponent * mpmath.log10(286_000)) + probability = (mpmath.floor(tail_raw) + mpmath.mpf("0.5")) / SCALE + root = mpmath.sqrt(2) * mpmath.erfinv(1 - 2 * probability) + x0 = nint(root * SCALE) + for off in (-1, 0, 1): + sign = 1 if (index + off) % 2 == 0 else -1 + vecs.append(cdf_vec(sign * (x0 + off), "tail_rounding_transition")) + + # Preserve independently observed worst production points as regressions. + for x in (-1_016_809_046_576, -1_291_830_207_302, + 1_016_809_046_576, 1_291_830_207_302): + vecs.append(cdf_vec(x, "observed_worst_regression")) + + # Zero, exact saturation points, and enormous public-domain inputs. vecs.append(cdf_vec(0, "zero")) - for x_real in [8.0, 8.5, 9.0, 10.0, 20.0, 37.0]: - x = int(x_real * SCALE) + for x in (8 * SCALE, 8 * SCALE + 1, 9 * SCALE, 10 * SCALE, + 20 * SCALE, 37 * SCALE, I128_MAX): vecs.append(cdf_vec(x, "extreme_tail")) vecs.append(cdf_vec(-x, "extreme_tail_neg")) + vecs.append(cdf_vec(I128_MIN, "i128_min")) # Fill to exactly 10000 while len(vecs) < 10000: - x = int(random.uniform(-8.0, 8.0) * SCALE) + x = rng.randint(-8 * SCALE, 8 * SCALE) vecs.append(cdf_vec(x, "fill")) vecs = vecs[:10000] save("adv_norm_cdf_vectors.json", {"suite": "adversarial", "function": "norm_cdf_poly", - "boundaries": boundaries, - "zones": "boundaries, piece_interiors, deep_tails, zero, extreme_tails"}, vecs) + "boundaries_raw": boundaries, + "zones": "all implementation seams, raw center, every piece interior, tail rounding transitions, saturation, i128 extrema", + "reference": "mpmath.ncdf at 60 decimal digits"}, vecs) # ============================================================ # POW — 10,000 vectors @@ -382,9 +625,10 @@ def pp_vec(x_lo, x_hi, w_lo, w_hi, cat): x = int(random.uniform(x_lo, x_hi) * SCALE) w = int(random.uniform(w_lo, w_hi) * SCALE) if x <= 0: return None - ref = nint(mpmath.power(mpmath.mpf(x) / SCALE, mpmath.mpf(w) / SCALE) * SCALE) - if ref > U128_MAX or ref <= 0: return None - return {"x": s(x), "w": s(w), "expected": s(ref), "category": cat} + # pow_product_hp evaluates x^w * x^(1-w), whose exact reference is x. + # The previous generator accidentally emitted x^w and made every + # adversarial identity check compare against the wrong function. + return {"x": s(x), "w": s(w), "expected": s(x), "category": cat} pad_to(vecs, 2500, lambda: pp_vec(0.5, 2.0, 0.001, 0.01, "extreme_low_w")) pad_to(vecs, 5000, lambda: pp_vec(0.5, 2.0, 0.99, 0.999, "extreme_high_w")) @@ -401,7 +645,25 @@ def pp_vec(x_lo, x_hi, w_lo, w_hi, cat): print(f"SolMath Adversarial Vector Generator (post-rewrite)") print(f"SCALE={SCALE}, mpmath precision={mpmath.mp.dps} digits\n") + if "--ln-1p-only" in sys.argv: + gen_ln_1p() + sys.exit(0) + if "--expm1-only" in sys.argv: + gen_expm1() + sys.exit(0) + if "--exp-only" in sys.argv: + gen_exp() + sys.exit(0) + if "--ln-only" in sys.argv: + gen_ln() + sys.exit(0) + if "--norm-cdf-only" in sys.argv: + gen_norm_cdf() + sys.exit(0) + gen_ln() + gen_ln_1p() + gen_expm1() gen_exp() gen_norm_cdf() gen_pow() diff --git a/scripts/generate_american_kbi_data.py b/scripts/generate_american_kbi_data.py new file mode 100644 index 0000000..aa420fa --- /dev/null +++ b/scripts/generate_american_kbi_data.py @@ -0,0 +1,427 @@ +#!/usr/bin/env python3 +"""Generate fixed normalized quadrature constants for Kim Boundary Integration.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import pathlib +import re + +import numpy as np + +SCALE = 1 << 40 + +# The generated Rust file is canonical release data. NumPy's Legendre solver +# and platform libm can differ by one or two Q40 integer units between +# macOS/ARM and Linux/x86 even with identical pinned package versions. The +# fallback check below still requires this exact canonical token fingerprint; +# the tolerance applies only to independently regenerated comparison values. +CANONICAL_RUST_SHA256 = "5a30c3d9325b5387653b3eca06bedaef4e9ad1e75a3871643b8971b720322b87" +MAX_PLATFORM_Q40_DRIFT = 2 + +ARRAY_PATTERN = re.compile( + r"pub\(crate\) const (?P[A-Z0-9_]+): " + r"\[(?P[iu][0-9]+); (?P[0-9]+)\] = " + r"\[(?P.*?)\];", + re.DOTALL, +) +SCALAR_PATTERN = re.compile( + r"pub\(crate\) const (?P[A-Z0-9_]+): " + r"(?Pusize|u32|i64) = (?P-?[0-9]+);" +) + +# Positive nine-node empirical cubature trained on the original 48 QdFp +# contracts, with an L2 penalty of 1e-3 toward the transformed Gauss rule and +# an exact constant-moment constraint. These are global integration weights, +# not prices or per-contract data; the live path still evaluates Kim's premium +# kernel at every node from the six option inputs. +KBI_QDFP_PRICE_WEIGHTS = np.asarray( + [ + 0.0008900537918775943, + 0.008897920422518062, + 0.037697527427042486, + 0.08998901646075187, + 0.15601907100053944, + 0.21122572839441212, + 0.22426233826951844, + 0.17985293251787932, + 0.0911654117154608, + ], + dtype=np.float64, +) + + +def q12(value: float) -> int: + return int(round(float(value) * SCALE)) + + +def rust_array(name: str, values: list[int], rust_type: str = "i64") -> str: + rows = [f"pub(crate) const {name}: [{rust_type}; {len(values)}] = ["] + rows.extend(f" {value}," for value in values) + rows.append("];\n") + return "\n".join(rows) + + +def rust_matrix(name: str, values: np.ndarray) -> str: + rows, columns = values.shape + output = [f"pub(crate) const {name}: [[i64; {columns}]; {rows}] = ["] + for row in values: + output.append(" [" + ", ".join(str(q12(value)) for value in row) + "],") + output.append("];\n") + return "\n".join(output) + + +def rust_triangle(name: str, values: np.ndarray) -> str: + count = values.shape[0] + flattened = [ + q12(values[row, column]) + for row in range(count) + for column in range(row + 1) + ] + return rust_array(name, flattened) + + +def normalized_rust(source: str) -> str: + """Normalize rustfmt-only differences in a generated token stream.""" + return "".join(source.split()).replace(",]", "]") + + +def parse_generated_arrays(source: str) -> dict[str, tuple[str, int, list[int]]]: + arrays: dict[str, tuple[str, int, list[int]]] = {} + for match in ARRAY_PATTERN.finditer(source): + name = match.group("name") + literals = re.findall(r"-?0x[0-9a-fA-F]+|-?[0-9]+", match.group("body")) + values = [int(value, 0) for value in literals] + arrays[name] = (match.group("type"), int(match.group("length")), values) + return arrays + + +def cross_platform_regeneration_drift(generated: str, committed: str) -> int: + """Return maximum Q40 drift or raise on any structural/data mismatch.""" + canonical_fingerprint = hashlib.sha256(normalized_rust(committed).encode()).hexdigest() + if canonical_fingerprint != CANONICAL_RUST_SHA256: + raise ValueError( + "committed KBI artifact is not the canonical release token stream: " + f"{canonical_fingerprint}" + ) + + generated_scalars = { + match.group("name"): (match.group("type"), int(match.group("value"))) + for match in SCALAR_PATTERN.finditer(generated) + } + committed_scalars = { + match.group("name"): (match.group("type"), int(match.group("value"))) + for match in SCALAR_PATTERN.finditer(committed) + } + if generated_scalars != committed_scalars: + raise ValueError("generated KBI scalar metadata differs from the canonical artifact") + + generated_arrays = parse_generated_arrays(generated) + committed_arrays = parse_generated_arrays(committed) + # The payload digest necessarily changes when a platform rounds any Q40 + # coefficient differently. The exact committed digest remains protected by + # CANONICAL_RUST_SHA256; compare the regenerated numerical arrays below. + generated_arrays.pop("KBI_DATA_SHA256", None) + committed_arrays.pop("KBI_DATA_SHA256", None) + if generated_arrays.keys() != committed_arrays.keys(): + raise ValueError("generated KBI array set differs from the canonical artifact") + + max_drift = 0 + for name, (rust_type, length, generated_values) in generated_arrays.items(): + committed_type, committed_length, committed_values = committed_arrays[name] + if rust_type != committed_type or length != committed_length: + raise ValueError(f"generated KBI array declaration differs for {name}") + if len(generated_values) != length or len(committed_values) != length: + raise ValueError(f"generated KBI array length is inconsistent for {name}") + + # Index arrays and the embedded digest must be byte-identical. Only + # signed Q40 coefficients can exhibit platform rounding drift. + allowed_drift = MAX_PLATFORM_Q40_DRIFT if rust_type == "i64" else 0 + for index, (generated_value, committed_value) in enumerate( + zip(generated_values, committed_values, strict=True) + ): + drift = abs(generated_value - committed_value) + if drift > allowed_drift: + raise ValueError( + f"generated KBI value differs for {name}[{index}]: " + f"{generated_value} versus {committed_value} ({drift} units)" + ) + max_drift = max(max_drift, drift) + return max_drift + + +def product_weights(times: np.ndarray, right_index: int) -> np.ndarray: + """Integrate linear hats exactly against 1/sqrt(t_i-s).""" + current_time = float(times[right_index]) + weights = np.zeros(right_index + 1, dtype=np.float64) + for interval in range(right_index): + left = float(times[interval]) + right = float(times[interval + 1]) + width = right - left + left_lag = current_time - left + right_lag = current_time - right + left_root = math.sqrt(left_lag) + right_root = math.sqrt(max(right_lag, 0.0)) + cubic_difference = left_lag * left_root - right_lag * right_root + root_difference = left_root - right_root + weights[interval] += ( + (2.0 / 3.0) * cubic_difference + - 2.0 * right_lag * root_difference + ) / width + weights[interval + 1] += ( + 2.0 * left_lag * root_difference + - (2.0 / 3.0) * cubic_difference + ) / width + return weights + + +def trapezoid_weights(times: np.ndarray, right_index: int) -> np.ndarray: + weights = np.zeros(right_index + 1, dtype=np.float64) + widths = np.diff(times[: right_index + 1]) + weights[:-1] += 0.5 * widths + weights[1:] += 0.5 * widths + return weights + + +def render( + nodes: int, + order: int, + boundary_order: int, + grading: float, + price_power: float, + bits: int, +) -> str: + global SCALE + SCALE = 1 << bits + gauss_x, gauss_w = np.polynomial.legendre.leggauss(order) + boundary_x, boundary_w = np.polynomial.legendre.leggauss(boundary_order) + boundary_y = 0.5 * (boundary_x + 1.0) + boundary_unit_weight = 0.5 * boundary_w + coordinate = np.linspace(0.0, 1.0, nodes + 1) + times = coordinate**grading + boundary_lag: list[float] = [] + boundary_inverse_sqrt_lag: list[float] = [] + boundary_regular_weight: list[float] = [] + boundary_singular_weight: list[float] = [] + boundary_left: list[int] = [] + boundary_fraction: list[float] = [] + boundary_candidate_log_factor: list[float] = [] + boundary_candidate_coefficient_factor: list[float] = [] + for index in range(1, nodes + 1): + current_time = float(times[index]) + lag_samples = current_time * boundary_y * boundary_y + sample_times = current_time - lag_samples + left_index = np.searchsorted(times[: index + 1], sample_times, side="right") - 1 + left_index = np.clip(left_index, 0, index - 1) + fractions = ( + (sample_times - times[left_index]) + / (times[left_index + 1] - times[left_index]) + ) + right_is_candidate = left_index + 1 == index + boundary_lag.extend(lag_samples.tolist()) + boundary_inverse_sqrt_lag.extend((1.0 / np.sqrt(lag_samples)).tolist()) + boundary_regular_weight.extend( + (2.0 * current_time * boundary_y * boundary_unit_weight).tolist() + ) + boundary_singular_weight.extend( + (2.0 * math.sqrt(current_time) * boundary_unit_weight).tolist() + ) + boundary_left.extend(left_index.astype(int).tolist()) + boundary_fraction.extend(fractions.tolist()) + boundary_candidate_log_factor.extend( + np.where(right_is_candidate, 1.0 - fractions, 1.0).tolist() + ) + boundary_candidate_coefficient_factor.extend( + np.where(right_is_candidate, fractions, 0.0).tolist() + ) + + # Globally transformed premium rule: lag=y^p removes the sharp valuation + # endpoint layer without paying for a composite rule on every boundary + # interval. The boundary is still reconstructed on all `nodes` points. + price_y = 0.5 * (gauss_x + 1.0) + price_unit_weight = 0.5 * gauss_w + price_lag = price_y**price_power + price_weights = ( + price_power * price_y ** (price_power - 1.0) * price_unit_weight + ) + if order != 9 or abs(price_power - 2.25) > 1.0e-15: + raise ValueError("the certified empirical price rule requires order=9, power=2.25") + price_weights = KBI_QDFP_PRICE_WEIGHTS.copy() + samples = 1.0 - price_lag + price_sqrt_lag = np.sqrt(price_lag) + price_boundary_left = np.searchsorted(times, samples, side="right") - 1 + price_boundary_left = np.clip(price_boundary_left, 0, nodes - 1) + interpolation_fraction = ( + (samples - times[price_boundary_left]) + / (times[price_boundary_left + 1] - times[price_boundary_left]) + ) + + normal_step = 0.125 + normal_grid = np.arange(49, dtype=np.float64) * normal_step + normal_cdf = np.asarray( + [0.5 * (1.0 + math.erf(value / math.sqrt(2.0))) for value in normal_grid] + ) + normal_pdf = np.exp(-0.5 * normal_grid * normal_grid) / math.sqrt(2.0 * math.pi) + normal_a = ( + 2.0 * normal_cdf[:-1] + - 2.0 * normal_cdf[1:] + + normal_step * (normal_pdf[:-1] + normal_pdf[1:]) + ) + normal_b = ( + -3.0 * normal_cdf[:-1] + + 3.0 * normal_cdf[1:] + - normal_step * (2.0 * normal_pdf[:-1] + normal_pdf[1:]) + ) + normal_c = normal_step * normal_pdf[:-1] + normal_d = normal_cdf[:-1] + + payload = { + "nodes": nodes, + "order": order, + "boundary_order": boundary_order, + "grading": grading, + "price_power": price_power, + "bits": bits, + "times": [q12(value) for value in times], + "boundary_y_squared_over_nodes_squared": [ + q12(value * value / (nodes * nodes)) for value in boundary_y + ], + "boundary_lag": [q12(value) for value in boundary_lag], + "boundary_inverse_sqrt_lag": [q12(value) for value in boundary_inverse_sqrt_lag], + "boundary_regular_weight": [q12(value) for value in boundary_regular_weight], + "boundary_singular_weight": [q12(value) for value in boundary_singular_weight], + "boundary_left": boundary_left, + "boundary_fraction": [q12(value) for value in boundary_fraction], + "boundary_candidate_log_factor": [q12(value) for value in boundary_candidate_log_factor], + "boundary_candidate_coefficient_factor": [ + q12(value) for value in boundary_candidate_coefficient_factor + ], + "price_lag": [q12(value) for value in price_lag], + "price_weights": [q12(value) for value in price_weights], + "normal_hermite": { + "step": normal_step, + "a": [q12(value) for value in normal_a], + "b": [q12(value) for value in normal_b], + "c": [q12(value) for value in normal_c], + "d": [q12(value) for value in normal_d], + }, + } + digest = hashlib.sha256( + json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest() + + sections = [ + "// @generated by scripts/generate_american_kbi_data.py; do not edit.", + f"// SHA-256: {digest}", + "", + f"pub(crate) const KBI_NODES: usize = {nodes};", + f"pub(crate) const KBI_PRICE_ORDER: usize = {order};", + f"pub(crate) const KBI_PRICE_POINTS: usize = {order};", + f"pub(crate) const KBI_BOUNDARY_ORDER: usize = {boundary_order};", + f"pub(crate) const KBI_BOUNDARY_POINTS: usize = {nodes * boundary_order};", + f"pub(crate) const KBI_DATA_BITS: u32 = {bits};", + f"pub(crate) const KBI_GRADING: i64 = {q12(grading)};", + f"pub(crate) const KBI_PRICE_POWER: i64 = {q12(price_power)};", + f"pub(crate) const KBI_DATA_SHA256: [u8; 32] = [{', '.join('0x' + digest[i:i+2] for i in range(0, 64, 2))},];", + "", + rust_array("KBI_TIME_FRACTION", [q12(value) for value in times]), + rust_array( + "KBI_BOUNDARY_Y_SQUARED_OVER_NODES_SQUARED", + [q12(value * value / (nodes * nodes)) for value in boundary_y], + ), + rust_array("KBI_BOUNDARY_LAG_FRACTION", [q12(value) for value in boundary_lag]), + rust_array( + "KBI_BOUNDARY_INV_SQRT_LAG_FRACTION", + [q12(value) for value in boundary_inverse_sqrt_lag], + ), + rust_array( + "KBI_BOUNDARY_REGULAR_WEIGHT", + [q12(value) for value in boundary_regular_weight], + ), + rust_array( + "KBI_BOUNDARY_SINGULAR_WEIGHT", + [q12(value) for value in boundary_singular_weight], + ), + rust_array("KBI_BOUNDARY_LEFT", boundary_left, "u8"), + rust_array( + "KBI_BOUNDARY_FRACTION", [q12(value) for value in boundary_fraction] + ), + rust_array( + "KBI_BOUNDARY_CANDIDATE_LOG_FACTOR", + [q12(value) for value in boundary_candidate_log_factor], + ), + rust_array( + "KBI_BOUNDARY_CANDIDATE_COEFFICIENT_FACTOR", + [q12(value) for value in boundary_candidate_coefficient_factor], + ), + rust_array("KBI_PRICE_LAG_FRACTION", [q12(value) for value in price_lag]), + rust_array("KBI_PRICE_SQRT_LAG_FRACTION", [q12(value) for value in price_sqrt_lag]), + rust_array("KBI_PRICE_INV_SQRT_LAG_FRACTION", [q12(1.0 / value) for value in price_sqrt_lag]), + rust_array("KBI_PRICE_WEIGHT", [q12(value) for value in price_weights]), + rust_array( + "KBI_PRICE_BOUNDARY_LEFT", + price_boundary_left.astype(int).tolist(), + "u8", + ), + rust_array("KBI_PRICE_BOUNDARY_FRACTION", [q12(value) for value in interpolation_fraction]), + rust_array("KBI_NORMAL_HERMITE_A", [q12(value) for value in normal_a]), + rust_array("KBI_NORMAL_HERMITE_B", [q12(value) for value in normal_b]), + rust_array("KBI_NORMAL_HERMITE_C", [q12(value) for value in normal_c]), + rust_array("KBI_NORMAL_HERMITE_D", [q12(value) for value in normal_d]), + ] + return "\n".join(sections) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--nodes", type=int, default=18) + parser.add_argument("--order", type=int, default=9) + parser.add_argument("--boundary-order", type=int, default=6) + parser.add_argument("--grading", type=float, default=2.0) + parser.add_argument("--price-power", type=float, default=2.25) + parser.add_argument("--bits", type=int, default=40) + parser.add_argument( + "--check", + type=pathlib.Path, + help="fail unless this Rust file has the generated token stream", + ) + parser.add_argument( + "--output", + type=pathlib.Path, + help="write the generated Rust artifact to this path", + ) + args = parser.parse_args() + generated = render( + args.nodes, + args.order, + args.boundary_order, + args.grading, + args.price_power, + args.bits, + ) + if args.check is None and args.output is None: + print(generated, end="") + return + if args.output is not None: + args.output.write_text(generated) + print(f"Wrote KBI artifact to {args.output}") + return + committed = args.check.read_text() + if normalized_rust(generated) == normalized_rust(committed): + print(f"KBI artifact matches {args.check}") + return + try: + max_drift = cross_platform_regeneration_drift(generated, committed) + except ValueError as error: + raise SystemExit(f"generated KBI artifact differs from {args.check}: {error}") from error + print( + f"KBI artifact matches canonical {args.check}; " + f"maximum platform regeneration drift is {max_drift} Q40 units" + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_asian_quantlib_vectors.py b/scripts/generate_asian_quantlib_vectors.py new file mode 100644 index 0000000..efcdc6d --- /dev/null +++ b/scripts/generate_asian_quantlib_vectors.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +"""Generate reproducible arithmetic-Asian references with QuantLib 1.41. + +The corpus prices unseasoned, continuously sampled arithmetic-average calls and +puts with QuantLib's ContinuousArithmeticAsianLevyEngine. This is the exact +QuantLib counterpart of `arithmetic_asian_price(..., averaging_time=t, +fixed_weight=0)`; future-starting and partially fixed contracts remain covered +by the independent high-precision moment tests because QuantLib's Levy engine +does not expose the former state directly. + +Outputs: + benchmark/asian_quantlib_vectors.json all 10,000 vectors + tests/asian_quantlib_reference.rs 500 evenly sampled cargo tests + +All persisted values are integer fixed point at SCALE=1e12. Re-running this +file with QuantLib 1.41 is deterministic and rewrites both artifacts. +""" + +import json +import math +import os +import random + +import QuantLib as ql + + +SCALE = 10**12 +SEED = 0x415349414E +COUNT = 10_000 +TEST_COUNT = 500 +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +JSON_PATH = os.path.join(ROOT, "benchmark", "asian_quantlib_vectors.json") +RUST_PATH = os.path.join(ROOT, "tests", "asian_quantlib_reference.rs") + + +def fp(value): + return int(round(value * SCALE)) + + +def build_pricer(): + today = ql.Date(1, 1, 2025) + ql.Settings.instance().evaluationDate = today + day_count = ql.Actual365Fixed() + spot = ql.SimpleQuote(100.0) + rate = ql.SimpleQuote(0.05) + dividend = ql.SimpleQuote(0.02) + vol = ql.SimpleQuote(0.40) + process = ql.BlackScholesMertonProcess( + ql.QuoteHandle(spot), + ql.YieldTermStructureHandle( + ql.FlatForward(today, ql.QuoteHandle(dividend), day_count) + ), + ql.YieldTermStructureHandle( + ql.FlatForward(today, ql.QuoteHandle(rate), day_count) + ), + ql.BlackVolTermStructureHandle( + ql.BlackConstantVol( + today, ql.NullCalendar(), ql.QuoteHandle(vol), day_count + ) + ), + ) + engine = ql.ContinuousArithmeticAsianLevyEngine( + process, ql.QuoteHandle(ql.SimpleQuote(0.0)), today + ) + + def price(s, k, r, q, sigma, days): + spot.setValue(s) + rate.setValue(r) + dividend.setValue(q) + vol.setValue(sigma) + exercise = ql.EuropeanExercise(today + days) + result = [] + for side in (ql.Option.Call, ql.Option.Put): + option = ql.ContinuousAveragingAsianOption( + ql.Average.Arithmetic, + today, + ql.PlainVanillaPayoff(side, k), + exercise, + ) + option.setPricingEngine(engine) + result.append(max(option.NPV(), 0.0)) + return result + + return price + + +def cases(): + # Explicit boundary/regression rows are followed by a deterministic, + # stratified production corpus. + rows = [ + (100.0, 100.0, 0.05, 0.02, 0.40, 365, "atm_1y"), + (120.0, 100.0, 0.10, 0.04, 0.80, 730, "long_high_vol"), + (80.0, 100.0, 0.0, 0.0, 0.20, 30, "short_otm"), + (100.0, 100.0, 0.0, 0.0, 0.05, 1, "one_day_low_vol"), + (50.0, 100.0, 0.12, 0.0, 1.50, 1825, "deep_otm"), + (150.0, 100.0, 0.0, 0.12, 1.50, 1825, "deep_itm"), + ] + rng = random.Random(SEED) + maturity_bands = [(1, 7), (8, 30), (31, 182), (183, 365), (366, 730), (731, 1825)] + money_bands = [(0.50, 0.75), (0.75, 0.95), (0.95, 1.05), (1.05, 1.25), (1.25, 1.50)] + vol_bands = [(0.05, 0.15), (0.15, 0.40), (0.40, 0.80), (0.80, 1.50)] + cells = [ + (maturity, money, vol) + for maturity in maturity_bands + for money in money_bands + for vol in vol_bands + ] + # Produce spare candidates because the QuantLib f64 engine can return NaN + # in a few very deep-tail configurations; those are rejected below. + while len(rows) < COUNT * 2: + maturity, money, vol = cells[(len(rows) - 6) % len(cells)] + k = rng.uniform(20.0, 500.0) + rows.append( + ( + k * rng.uniform(*money), + k, + rng.uniform(0.0, 0.12), + rng.uniform(0.0, 0.12), + rng.uniform(*vol), + rng.randint(*maturity), + "stratified", + ) + ) + return rows + + +def generate(): + price = build_pricer() + vectors = [] + for s, k, r, q, sigma, days, category in cases(): + call, put = price(s, k, r, q, sigma, days) + if not (math.isfinite(call) and math.isfinite(put)): + continue + t = days / 365.0 + vectors.append( + { + "s": str(fp(s)), + "k": str(fp(k)), + "r": str(fp(r)), + "q": str(fp(q)), + "sigma": str(fp(sigma)), + "t": str(fp(t)), + "averaging_time": str(fp(t)), + "fixed_average": "0", + "fixed_weight": "0", + "ql_call": str(fp(call)), + "ql_put": str(fp(put)), + "t_days": days, + "category": category, + } + ) + if len(vectors) == COUNT: + break + + if len(vectors) != COUNT: + raise RuntimeError(f"generated only {len(vectors)} finite QuantLib vectors") + + payload = { + "meta": { + "reference": ( + f"QuantLib {ql.__version__} ContinuousArithmeticAsianLevyEngine" + ), + "quantlib_version": ql.__version__, + "evaluation_date": "2025-01-01", + "day_count": "Actual/365 (Fixed)", + "average": "continuous arithmetic, unseasoned, averaging starts at valuation", + "scale": SCALE, + "seed": SEED, + "count": len(vectors), + "generator": "scripts/generate_asian_quantlib_vectors.py", + }, + "vectors": vectors, + } + with open(JSON_PATH, "w", encoding="utf-8") as handle: + json.dump(payload, handle, indent=2) + handle.write("\n") + + step = max(1, len(vectors) // TEST_COUNT) + selected = vectors[::step][:TEST_COUNT] + with open(RUST_PATH, "w", encoding="utf-8") as handle: + handle.write( + "//! Auto-generated QuantLib arithmetic-Asian reference tests.\n" + "//! Source: scripts/generate_asian_quantlib_vectors.py\n\n" + '#![cfg(feature = "asian")]\n\n' + "use solmath::arithmetic_asian_price;\n\n" + "#[test]\n" + "fn matches_quantlib_1_41_continuous_arithmetic_levy_engine() {\n" + " const VECTORS: &[[u128; 11]] = &[\n" + ) + for vector in selected: + handle.write( + " [" + + ", ".join( + vector[key] + for key in ( + "s", + "k", + "r", + "q", + "sigma", + "t", + "averaging_time", + "fixed_average", + "fixed_weight", + "ql_call", + "ql_put", + ) + ) + + "],\n" + ) + handle.write( + " ];\n" + " // Covers the full-corpus max (<595,000,000 raw), which occurs\n" + " // in QuantLib's cancellation-sensitive two-day Levy tail.\n" + " const TOLERANCE: u128 = 1_000_000_000; // $0.001\n" + " let mut max_call = (0u128, 0usize);\n" + " let mut max_put = (0u128, 0usize);\n" + " for (index, vector) in VECTORS.iter().enumerate() {\n" + " let [s, k, r, q, sigma, t, averaging_time, fixed_average, fixed_weight, expected_call, expected_put] = *vector;\n" + " let actual = arithmetic_asian_price(s, k, r, q, sigma, t, averaging_time, fixed_average, fixed_weight).unwrap();\n" + " let call_diff = actual.call.abs_diff(expected_call);\n" + " let put_diff = actual.put.abs_diff(expected_put);\n" + " if call_diff > max_call.0 { max_call = (call_diff, index); }\n" + " if put_diff > max_put.0 { max_put = (put_diff, index); }\n" + " }\n" + " eprintln!(\"max QuantLib diffs: call={} at {}, put={} at {}\", max_call.0, max_call.1, max_put.0, max_put.1);\n" + " assert!(max_call.0 <= TOLERANCE, \"QuantLib call max diff={} at {}\", max_call.0, max_call.1);\n" + " assert!(max_put.0 <= TOLERANCE, \"QuantLib put max diff={} at {}\", max_put.0, max_put.1);\n" + "}\n" + ) + + print(f"QuantLib {ql.__version__}: {len(vectors):,} vectors -> {JSON_PATH}") + print(f"Cargo subset: {len(selected):,} vectors -> {RUST_PATH}") + + +if __name__ == "__main__": + if ql.__version__ != "1.41": + raise SystemExit(f"QuantLib 1.41 required, found {ql.__version__}") + generate() diff --git a/scripts/generate_asian_vectors.py b/scripts/generate_asian_vectors.py new file mode 100644 index 0000000..3325808 --- /dev/null +++ b/scripts/generate_asian_vectors.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python3 +"""Generate SolMath Asian/TWAP accuracy corpora. + +Exactly 100,000 stratified production vectors and 10,000 adversarial vectors +are evaluated from the fixed-point inputs with mpmath at 60 decimal digits. +These corpora validate the compiled implementation of the stated continuous +GBM moment-match model. The separate QuantLib generator remains an independent +cross-check for the unseasoned subset supported by QuantLib's Levy engine. +""" + +from __future__ import annotations + +import json +import random +from itertools import product +from pathlib import Path + +import mpmath as mp + + +SCALE = 10**12 +PRODUCTION_COUNT = 100_000 +ADVERSARIAL_COUNT = 10_000 +SEED = 0xA51A2026 +ROOT = Path(__file__).resolve().parents[1] +OUTPUT = ROOT / "benchmark" + + +def raw(value: float) -> int: + return max(1, round(value * SCALE)) + + +def round_raw(value: mp.mpf) -> int: + return int(mp.floor(value * SCALE + mp.mpf("0.5"))) + + +def reference(values: list[int]) -> list[int]: + spot, strike, rate, yield_rate, sigma, time, window, fixed_average, weight = [ + mp.mpf(value) / SCALE for value in values + ] + discount = mp.exp(-rate * time) + if weight == 1: + mean = fixed_average + call = discount * max(mean - strike, 0) + put = discount * max(strike - mean, 0) + return [round_raw(call), round_raw(put), round_raw(mean), 0] + + carry = rate - yield_rate + start = time - window + + def phi1(value: mp.mpf) -> mp.mpf: + return mp.expm1(value) / value if value else mp.mpf(1) + + b_window = carry * window + variance_window = sigma * sigma * window + future_mean = spot * mp.exp(carry * start) * phi1(b_window) + if b_window: + second_kernel = 2 / b_window * ( + mp.exp(b_window) * phi1(b_window + variance_window) + - phi1(2 * b_window + variance_window) + ) + elif variance_window: + second_kernel = ( + 2 * (mp.expm1(variance_window) - variance_window) / variance_window**2 + ) + else: + second_kernel = mp.mpf(1) + future_second = ( + spot**2 + * mp.exp((2 * carry + sigma**2) * start) + * second_kernel + ) + mean = weight * fixed_average + (1 - weight) * future_mean + variance = max((1 - weight) ** 2 * (future_second - future_mean**2), 0) + log_variance = mp.log1p(variance / mean**2) + if log_variance: + root_variance = mp.sqrt(log_variance) + d1 = (mp.log(mean / strike) + log_variance / 2) / root_variance + d2 = d1 - root_variance + normal = lambda value: mp.erfc(-value / mp.sqrt(2)) / 2 + call = discount * (mean * normal(d1) - strike * normal(d2)) + else: + call = discount * max(mean - strike, 0) + put = call - discount * (mean - strike) + return [round_raw(call), round_raw(put), round_raw(mean), round_raw(log_variance)] + + +def vector( + spot: float, + strike: float, + rate: float, + yield_rate: float, + sigma: float, + time: float, + window: float, + fixed_average: float, + weight: float, + category: str, +) -> dict[str, str | int]: + values = [ + raw(spot), + raw(strike), + max(0, round(rate * SCALE)), + max(0, round(yield_rate * SCALE)), + raw(sigma), + raw(time), + 0 if weight == 1 else raw(min(time, window)), + 0 if weight == 0 else raw(fixed_average), + SCALE if weight == 1 else max(0, min(SCALE - 1, round(weight * SCALE))), + ] + return vector_raw(values, category) + + +def vector_raw(values: list[int], category: str) -> dict[str, str | int]: + """Build a vector without losing deliberately chosen raw-unit seams.""" + expected = reference(values) + keys = ( + "s", + "k", + "r", + "q", + "sigma", + "t", + "averaging_time", + "fixed_average", + "fixed_weight", + ) + row: dict[str, str | int] = {key: str(value) for key, value in zip(keys, values)} + row.update( + { + "expected_call": str(expected[0]), + "expected_put": str(expected[1]), + "expected_mean": str(expected[2]), + "expected_log_variance": str(expected[3]), + "category": category, + } + ) + return row + + +def production_vectors(rng: random.Random) -> list[dict[str, str | int]]: + money = [(0.50, 0.75), (0.75, 0.95), (0.95, 1.05), (1.05, 1.25), (1.25, 1.50)] + rates = [(0.0, 0.02), (0.02, 0.08), (0.08, 0.20)] + yields = [(0.0, 0.02), (0.02, 0.08), (0.08, 0.20)] + vols = [(0.05, 0.15), (0.15, 0.40), (0.40, 0.80), (0.80, 2.00)] + maturities = [(1 / 365, 30 / 365), (30 / 365, 0.5), (0.5, 1.0), (1.0, 2.0)] + window_ratios = [(0.0001, 0.01), (0.01, 0.25), (0.25, 0.75), (0.75, 1.0)] + fixing_modes = ("unseasoned", "partial_low", "partial_high", "fully_fixed") + cells = list(product(money, rates, yields, vols, maturities, window_ratios, fixing_modes)) + result = [] + for index in range(PRODUCTION_COUNT): + money_band, rate_band, yield_band, vol_band, maturity_band, window_band, mode = cells[ + index % len(cells) + ] + spot = rng.uniform(20, 500) + strike = spot / rng.uniform(*money_band) + rate = rng.uniform(*rate_band) + yield_rate = rng.uniform(*yield_band) + sigma = rng.uniform(*vol_band) + time = rng.uniform(*maturity_band) + window = time * rng.uniform(*window_band) + if mode == "unseasoned": + weight, fixed_average = 0.0, 0.0 + elif mode == "partial_low": + weight = rng.uniform(0.01, 0.50) + fixed_average = spot * rng.uniform(0.70, 1.30) + elif mode == "partial_high": + weight = rng.uniform(0.50, 0.95) + fixed_average = spot * rng.uniform(0.70, 1.30) + else: + weight = 1.0 + fixed_average = spot * rng.uniform(0.70, 1.30) + result.append( + vector( + spot, + strike, + rate, + yield_rate, + sigma, + time, + window, + fixed_average, + weight, + mode, + ) + ) + return result + + +def adversarial_vectors(rng: random.Random) -> list[dict[str, str | int]]: + result = [] + category_count = 11 + base_count, extra = divmod(ADVERSARIAL_COUNT, category_count) + for category_index in range(category_count): + lane_count = base_count + (1 if category_index < extra else 0) + for lane in range(lane_count): + spot = rng.uniform(1, 1_000) + strike = spot * rng.uniform(0.20, 2.00) + rate = rng.uniform(0, 0.20) + yield_rate = rng.uniform(0, 0.20) + sigma = rng.uniform(0.05, 2.0) + time = rng.uniform(1 / 365, 2.0) + window = time * rng.uniform(0.001, 1.0) + weight = rng.uniform(0.01, 0.99) + fixed_average = spot * rng.uniform(0.50, 1.50) + + if category_index == 0: + category = "tiny_window" + window = rng.choice([1 / SCALE, 2 / SCALE, 30 / (365 * 24 * 60)]) + elif category_index == 1: + category = "future_start" + time = rng.uniform(2.0, 10.0) + window = rng.uniform(30 / (365 * 24 * 60), 1 / 365) + sigma = rng.uniform(0.05, 1.0) + rate, yield_rate, weight, fixed_average = rng.uniform(0, 0.10), rng.uniform(0, 0.10), 0.0, 0.0 + elif category_index in (2, 3): + category = "series_below" if category_index == 2 else "series_above" + sigma = rng.uniform(0.40, 1.50) + carry = rng.choice([-1, 1]) * rng.uniform(0.01, 0.15) + rate = 0.16 + min(carry, 0) + yield_rate = rate - carry + coefficient = abs(carry + sigma * sigma) + abs(carry) + seam = 0.249999 if category_index == 2 else 0.250001 + window = seam / coefficient + time = window + rng.uniform(0, 1.0) + elif category_index == 4: + category = "zero_carry" + rate = yield_rate = rng.uniform(0, 0.20) + elif category_index == 5: + category = "carry_raw_seam" + base_raw = rng.randint(0, 200_000_000_000) + if lane % 2: + rate, yield_rate = (base_raw + 1) / SCALE, base_raw / SCALE + else: + rate, yield_rate = base_raw / SCALE, (base_raw + 1) / SCALE + elif category_index == 6: + category = "fixing_weight_seam" + weight = rng.choice([1 / SCALE, 2 / SCALE, (SCALE - 2) / SCALE, (SCALE - 1) / SCALE]) + elif category_index == 7: + category = "deep_tail_low_vol" + strike = spot * rng.choice([rng.uniform(0.05, 0.20), rng.uniform(3.0, 8.0)]) + sigma = rng.uniform(0.001, 0.10) + time = rng.uniform(1 / 365, 0.25) + window = time + weight, fixed_average = 0.0, 0.0 + elif category_index == 8: + category = "high_variance" + sigma = rng.uniform(2.0, 6.0) + time = rng.uniform(0.005, min(0.75, 30 / (sigma * sigma))) + window = time * rng.uniform(0.25, 1.0) + rate, yield_rate = rng.uniform(0, 0.10), rng.uniform(0, 0.10) + elif category_index == 9: + category = "partial_fixing_cdf_sensitivity" + if lane == 0: + # Retain the production maximizer that exposed this missing + # regime. One raw unit of log-variance error is amplified + # near the ATM CDF transition when little variance remains. + result.append( + vector_raw( + [ + 498_985_480_275_581, + 491_004_255_994_972, + 19_922_217_555, + 41_549_074_800, + 95_483_712_338, + 33_551_608_118, + 27_155_779_124, + 490_348_271_125_054, + 783_463_001_134, + ], + category, + ) + ) + continue + + spot = rng.uniform(100, 999) + rate = rng.uniform(0, 0.08) + yield_rate = rng.uniform(0, 0.08) + sigma = rng.uniform(0.005, 0.15) + time = rng.uniform(1 / 365, 0.10) + window = time * rng.uniform(0.20, 1.0) + weight = 1 - 10 ** rng.uniform(-3.0, -0.45) + fixed_average = spot * rng.uniform(0.90, 1.10) + + # Place the strike at and immediately around the matched + # distribution's CDF transition. Compute the mean/variance from + # the exact raw inputs before choosing K; K does not affect them. + values = [ + raw(spot), + 1, + round(rate * SCALE), + round(yield_rate * SCALE), + raw(sigma), + raw(time), + raw(min(time, window)), + raw(fixed_average), + max(1, min(SCALE - 1, round(weight * SCALE))), + ] + moments = reference(values) + mean_raw, log_variance_raw = moments[2], moments[3] + standard_deviation_raw = round( + mean_raw * (mp.expm1(mp.mpf(log_variance_raw) / SCALE) ** mp.mpf("0.5")) + ) + z = (0, 0.05, -0.05, 0.20, -0.20, 0.50, -0.50, 1.0, -1.0)[lane % 9] + raw_nudge = (-2, -1, 0, 1, 2)[(lane // 9) % 5] + values[1] = max(1, round(mean_raw + z * standard_deviation_raw) + raw_nudge) + result.append(vector_raw(values, category)) + continue + else: + category = "fully_fixed" + weight = 1.0 + window = 0.0 + + result.append( + vector( + spot, + strike, + rate, + yield_rate, + sigma, + time, + window, + fixed_average, + weight, + category, + ) + ) + return result + + +def save(filename: str, kind: str, vectors: list[dict[str, str | int]]) -> None: + payload = { + "meta": { + "reference": "mpmath 1.4.1, 60 decimal digits, independent continuous-GBM moment match", + "kind": kind, + "scale": SCALE, + "seed": SEED, + "count": len(vectors), + "generator": "scripts/generate_asian_vectors.py", + }, + "vectors": vectors, + } + path = OUTPUT / filename + with path.open("w", encoding="utf-8") as handle: + json.dump(payload, handle, separators=(",", ":")) + handle.write("\n") + print(f"{len(vectors):,} -> {path}") + + +def main() -> None: + mp.mp.dps = 60 + rng = random.Random(SEED) + save("prod_asian_vectors.json", "stratified production", production_vectors(rng)) + save("adv_asian_vectors.json", "adversarial seams and tails", adversarial_vectors(rng)) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_bvn_phi2_references.py b/scripts/generate_bvn_phi2_references.py new file mode 100644 index 0000000..84bbb47 --- /dev/null +++ b/scripts/generate_bvn_phi2_references.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Regenerate the independent BVN/Phi2 audit corpora. + +The 22,500-case BVN corpus exactly preserves the 2026-07-11 audit seed, +stratification, adaptive-quadrature tolerances, and row order. The separate +10,000-case Phi2 corpus uses five fixed correlations with off-grid thresholds. +""" + +import argparse +import json +import math +import random +from concurrent.futures import ProcessPoolExecutor + +from scipy.integrate import quad +from scipy.special import ndtr + + +SCALE = 10**12 + + +def reference(row: tuple[float, float, float]) -> dict[str, int]: + a, b, rho = row + if rho >= 1.0: + probability = float(ndtr(min(a, b))) + elif rho <= -1.0: + probability = max(0.0, float(ndtr(a) + ndtr(b) - 1.0)) + elif rho == 0.0: + probability = float(ndtr(a) * ndtr(b)) + else: + alpha = math.asin(rho) + + def integrand(theta: float) -> float: + sin_theta = math.sin(theta) + cos_theta = math.cos(theta) + exponent = -( + a * a - 2.0 * a * b * sin_theta + b * b + ) / (2.0 * cos_theta * cos_theta) + return 0.0 if exponent < -745.0 else math.exp(exponent) + + integral = quad( + integrand, + 0.0, + alpha, + epsabs=2e-15, + epsrel=2e-14, + limit=250, + )[0] + probability = float(ndtr(a) * ndtr(b) + integral / (2.0 * math.pi)) + probability = min(1.0, max(0.0, probability)) + return { + "a": round(a * SCALE), + "b": round(b * SCALE), + "rho": round(rho * SCALE), + "expected": round(probability * SCALE), + } + + +def build_inputs() -> tuple[list[tuple[float, float, float]], list[tuple[float, float, float]]]: + rng = random.Random(20260710) + bvn: list[tuple[float, float, float]] = [] + rho_buckets = [ + (-0.99, -0.95), + (-0.95, -0.8), + (-0.8, -0.2), + (-0.2, 0.2), + (0.2, 0.8), + (0.8, 0.95), + (0.95, 0.99), + ] + for low, high in rho_buckets: + for _ in range(2_500): + bvn.append( + (rng.uniform(-4.0, 4.0), rng.uniform(-4.0, 4.0), rng.uniform(low, high)) + ) + + # Near-singular unequal and equal-threshold boundary layers. + for sign in (-1, 1): + for exponent in (6, 8, 10, 12): + rho = sign * (1.0 - 10.0 ** (-exponent)) + for _ in range(500): + a = rng.uniform(-3.5, 3.5) + b = (-a if sign < 0 else a) + rng.uniform(-2e-3, 2e-3) + bvn.append((a, b, rho)) + + # Exact endpoints and their analytic identities. + for sign in (-1, 1): + for _ in range(500): + bvn.append((rng.uniform(-4.0, 4.0), rng.uniform(-4.0, 4.0), float(sign))) + + phi2: list[tuple[float, float, float]] = [] + for rho in (-0.9, -0.5, 0.0, 0.5, 0.9): + for _ in range(2_000): + phi2.append((rng.uniform(-4.0, 4.0), rng.uniform(-4.0, 4.0), rho)) + return bvn, phi2 + + +def write_corpus(path: str, rows: list[dict[str, int]], kind: str) -> None: + with open(path, "w", encoding="utf-8") as output: + json.dump( + { + "meta": { + "kind": kind, + "vectors": len(rows), + "seed": 20260710, + "scale": SCALE, + "reference": "SciPy ndtr plus adaptive angular quadrature", + "scipy_quad_epsabs": 2e-15, + "scipy_quad_epsrel": 2e-14, + }, + "vectors": rows, + }, + output, + separators=(",", ":"), + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--bvn-output", default="/tmp/bvn-audit-vectors.json") + parser.add_argument("--phi2-output", default="/tmp/phi2-audit-vectors.json") + parser.add_argument("--workers", type=int, default=None) + args = parser.parse_args() + + bvn_inputs, phi2_inputs = build_inputs() + with ProcessPoolExecutor(max_workers=args.workers) as executor: + bvn_rows = list(executor.map(reference, bvn_inputs, chunksize=64)) + with ProcessPoolExecutor(max_workers=args.workers) as executor: + phi2_rows = list(executor.map(reference, phi2_inputs, chunksize=64)) + + write_corpus(args.bvn_output, bvn_rows, "bvn") + write_corpus(args.phi2_output, phi2_rows, "phi2_off_grid") + print(f"bvn={len(bvn_rows)} phi2={len(phi2_rows)}") + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_exp_coeffs.py b/scripts/generate_exp_coeffs.py new file mode 100644 index 0000000..8089fa0 --- /dev/null +++ b/scripts/generate_exp_coeffs.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""Generate the compact N32/Q22 coefficient kernel for ``exp_fixed_i``. + +The local polynomial is the degree-5 real minimax approximation to ``exp`` on +``[-ln(2)/64, ln(2)/64]``. A high-precision Remez exchange derives it; the +stored coefficients are then rounded to ``SCALE * 2^22``. The 32 phase +constants reconstruct ``2^(phase/32)`` at Q62. +""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +import mpmath as mp + + +ROOT = Path(__file__).resolve().parents[1] +DESTINATION = ROOT / "src" / "exp_coeffs.rs" +SCALE = 10**12 +LN2_RAW = 693_147_180_560 +PHASE_BITS = 5 +PHASES = 1 << PHASE_BITS +POLY_GUARD = 22 +POLY_DEGREE = 5 +WORK_DPS = 120 + + +def round_away(value: mp.mpf) -> int: + """Round to nearest, with exact half cases away from zero.""" + if value >= 0: + return int(mp.floor(value + mp.mpf("0.5"))) + return int(mp.ceil(value - mp.mpf("0.5"))) + + +def solve_exchange(extrema: list[mp.mpf], degree: int) -> tuple[list[mp.mpf], mp.mpf]: + matrix = mp.matrix(degree + 2, degree + 2) + target = mp.matrix(degree + 2, 1) + for row, x in enumerate(extrema): + for power in range(degree + 1): + matrix[row, power] = x**power + matrix[row, degree + 1] = (-1) ** row + target[row, 0] = mp.exp(x) + solution = mp.lu_solve(matrix, target) + return [solution[index, 0] for index in range(degree + 1)], solution[degree + 1, 0] + + +def derivative_roots( + coefficients: list[mp.mpf], radius: mp.mpf, degree: int +) -> list[mp.mpf]: + def derivative_error(x: mp.mpf) -> mp.mpf: + return ( + sum( + power * coefficients[power] * x ** (power - 1) + for power in range(1, degree + 1) + ) + - mp.exp(x) + ) + + grid = [-radius + 2 * radius * index / 4096 for index in range(4097)] + values = [derivative_error(x) for x in grid] + roots: list[mp.mpf] = [] + for left, right, f_left, f_right in zip(grid, grid[1:], values, values[1:]): + if f_left * f_right < 0: + root = mp.findroot(derivative_error, (left, right), verify=False) + if not roots or abs(root - roots[-1]) > mp.mpf("1e-90"): + roots.append(root) + assert len(roots) == degree, (len(roots), degree) + return roots + + +def derive_remez() -> tuple[list[mp.mpf], mp.mpf, list[mp.mpf]]: + """Return ascending power coefficients, alternation error and extrema.""" + mp.mp.dps = WORK_DPS + radius = mp.log(2) / (2 * PHASES) + extrema = sorted( + radius * mp.cos(mp.pi * index / (POLY_DEGREE + 1)) + for index in range(POLY_DEGREE + 2) + ) + for _ in range(30): + coefficients, error = solve_exchange(extrema, POLY_DEGREE) + updated = [-radius] + updated.extend(derivative_roots(coefficients, radius, POLY_DEGREE)) + updated.append(radius) + movement = max(abs(left - right) for left, right in zip(extrema, updated)) + extrema = updated + if movement < mp.mpf("1e-100"): + break + else: # pragma: no cover - deterministic convergence guard + raise AssertionError("Remez exchange did not converge") + + coefficients, error = solve_exchange(extrema, POLY_DEGREE) + alternating = [ + sum(coefficient * x**power for power, coefficient in enumerate(coefficients)) + - mp.exp(x) + for x in extrema + ] + magnitudes = [abs(value) for value in alternating] + assert max(magnitudes) - min(magnitudes) < mp.mpf("1e-100") + assert all(left * right < 0 for left, right in zip(alternating, alternating[1:])) + return coefficients, abs(error), extrema + + +def rust_int(value: int) -> str: + sign = "-" if value < 0 else "" + return f"{sign}{abs(value):_d}" + + +def render() -> str: + mp.mp.dps = WORK_DPS + ln2 = mp.log(2) + coefficients, _, _ = derive_remez() + guarded_scale = SCALE << POLY_GUARD + quantized = [round_away(value * guarded_scale) for value in coefficients] + quantized.reverse() + phases = [round_away(mp.power(2, mp.mpf(index) / PHASES) * (1 << 62)) for index in range(PHASES)] + + raw_to_q64 = mp.mpf(1 << 64) / SCALE + raw_to_q63_hi = int(mp.floor(raw_to_q64)) + raw_to_q63_frac_q28 = round_away((raw_to_q64 - raw_to_q63_hi) * (1 << 28)) + ln2_residual_q96 = round_away((ln2 - mp.mpf(LN2_RAW) / SCALE) * (1 << 96)) + step_q63 = round_away(ln2 / PHASES * (1 << 63)) + + rows = [ + "// @generated by scripts/generate_exp_coeffs.py; do not edit manually.", + "", + "/// Split multiplier for converting a raw SCALE residual to Q63 without a", + "/// wide multiply. Together these approximate `2^64 / SCALE`.", + f"pub(crate) const EXP_RAW_TO_Q63_HI: i64 = {rust_int(raw_to_q63_hi)};", + f"pub(crate) const EXP_RAW_TO_Q63_FRAC_Q28: i64 = {rust_int(raw_to_q63_frac_q28)};", + "", + "/// `round((ln(2) - LN2_I / SCALE) * 2^96)`.", + f"pub(crate) const EXP_LN2_RESIDUAL_Q96: i64 = {rust_int(ln2_residual_q96)};", + "", + "/// `round((ln(2) / 32) * 2^63)`.", + f"pub(crate) const EXP_STEP_Q63: i64 = {rust_int(step_q63)};", + "", + f"pub(crate) const EXP_POLY_GUARD: i32 = {POLY_GUARD};", + f"pub(crate) const EXP_PHASE_BITS: i32 = {PHASE_BITS};", + "pub(crate) const EXP_PHASES: usize = 1 << EXP_PHASE_BITS;", + "", + "/// Degree-5 near-minimax coefficients for exp(r) on", + "/// `[-ln(2)/64, ln(2)/64]`, descending, at `SCALE * 2^22`.", + "pub(crate) const EXP_REMEZ_Q22: [i64; 6] = [", + ] + rows.extend(f" {rust_int(value)}," for value in quantized) + rows.extend( + [ + "];", + "", + "/// `round(2^(phase/32) * 2^62)`. These are reconstruction constants,", + "/// not sampled values of the exponential kernel.", + "pub(crate) const EXP2_PHASE_Q62: [i64; EXP_PHASES] = [", + ] + ) + rows.extend(f" {rust_int(value)}," for value in phases) + rows.extend(["];", ""]) + return "\n".join(rows) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--check", action="store_true", help="fail if the checked-in source differs") + parser.add_argument("--stdout", action="store_true", help="print instead of writing the source") + args = parser.parse_args() + output = render() + if args.stdout: + print(output, end="") + elif args.check: + assert DESTINATION.read_text() == output, f"{DESTINATION} is stale" + print(f"verified {DESTINATION}") + else: + DESTINATION.write_text(output) + print(f"wrote {DESTINATION}") + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_expm1_lut.py b/scripts/generate_expm1_lut.py new file mode 100644 index 0000000..36b3a2a --- /dev/null +++ b/scripts/generate_expm1_lut.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Generate the division-free raw-Q22/Q43 expm1 reduction tables.""" + +from pathlib import Path + +import mpmath as mp + + +SCALE = 10**12 +RAW_Q22 = SCALE << 22 +LN2 = mp.log(2) +R_MIN = -346_573_590_280 +STEP = 1 << 29 +SEGMENTS = 1292 + + +def round_away(value: mp.mpf) -> int: + if value >= 0: + return int(mp.floor(value + mp.mpf("0.5"))) + return int(mp.ceil(value - mp.mpf("0.5"))) + + +def render(name: str, ty: str, values: list[int], length: str | None = None) -> str: + rows = [f" {value}," for value in values] + array_length = length or str(len(values)) + return f"pub(crate) const {name}: [{ty}; {array_length}] = [\n" + "\n".join(rows) + "\n];\n" + + +def main() -> None: + mp.mp.dps = 80 + mid_exp_raw_q22 = [] + for j in range(SEGMENTS): + midpoint_raw = R_MIN + j * STEP + STEP // 2 + mid_exp_raw_q22.append(round_away(mp.exp(mp.mpf(midpoint_raw) / SCALE) * RAW_Q22)) + + inv_ln2_q56 = round_away(mp.mpf(1 << 56) / (LN2 * SCALE)) + raw_to_q43_g31 = round_away(mp.mpf(1 << 74) / SCALE) + + output = f"""// @generated by scripts/generate_expm1_lut.py; do not edit manually. + +pub(crate) const EXPM1_R_MIN: i64 = {R_MIN}; +pub(crate) const EXPM1_LUT_STEP_SHIFT: u32 = 29; +pub(crate) const EXPM1_LUT_STEP: i64 = {STEP}; +pub(crate) const EXPM1_LUT_SEGMENTS: usize = {SEGMENTS}; +pub(crate) const EXPM1_INV_LN2_Q56: i64 = {inv_ln2_q56}; +pub(crate) const EXPM1_RAW_TO_Q43_G31: i64 = {raw_to_q43_g31}; + +""" + output += render("EXPM1_MID_EXP_RAW_Q22", "i64", mid_exp_raw_q22, "EXPM1_LUT_SEGMENTS") + Path(__file__).resolve().parents[1].joinpath("src/expm1_lut.rs").write_text(output) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_ln2_lut.py b/scripts/generate_ln2_lut.py new file mode 100644 index 0000000..3cec3c5 --- /dev/null +++ b/scripts/generate_ln2_lut.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Generate the shared rounded k*ln(2) range-reduction table.""" + +from pathlib import Path + +import mpmath as mp + + +SCALE = 10**12 +K_MIN = -64 +K_MAX = 88 + + +def round_away(value: mp.mpf) -> int: + if value >= 0: + return int(mp.floor(value + mp.mpf("0.5"))) + return int(mp.ceil(value - mp.mpf("0.5"))) + + +def main() -> None: + mp.mp.dps = 80 + values = [ + round_away(mp.mpf(k) * mp.log(2) * SCALE) + for k in range(K_MIN, K_MAX + 1) + ] + rows = "\n".join(f" {value}," for value in values) + output = f"""// @generated by scripts/generate_ln2_lut.py; do not edit manually. +// Shared by ln_1p_fixed and expm1_fixed after power-of-two range reduction. + +pub(crate) const K_LN2_MIN: i32 = {K_MIN}; +pub(crate) const K_LN2_MAX: i32 = {K_MAX}; +pub(crate) const K_LN2_ENTRIES: usize = {len(values)}; +pub(crate) const K_LN2_RAW: [i64; K_LN2_ENTRIES] = [ +{rows} +]; +""" + Path(__file__).resolve().parents[1].joinpath("src/ln2_lut.rs").write_text(output) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_ln_lut.py b/scripts/generate_ln_lut.py new file mode 100644 index 0000000..86d63ad --- /dev/null +++ b/scripts/generate_ln_lut.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Generate the division-free Q42 log1p midpoint table.""" + +from pathlib import Path + +import mpmath as mp + + +SCALE = 10**12 +RECIP_SHIFT = 32 +Q42 = 1 << 42 +SEGMENTS = 1024 +STEP = SCALE // SEGMENTS +HALF_STEP = STEP // 2 + + +def round_away(value: mp.mpf) -> int: + if value >= 0: + return int(mp.floor(value + mp.mpf("0.5"))) + return int(mp.ceil(value - mp.mpf("0.5"))) + + +def render(name: str, ty: str, values: list[int], length: str | None = None) -> str: + rows = [f" {value}," for value in values] + array_length = length or str(len(values)) + return f"pub(crate) const {name}: [{ty}; {array_length}] = [\n" + "\n".join(rows) + "\n];\n" + + +def main() -> None: + mp.mp.dps = 80 + logs: list[int] = [] + reciprocals: list[int] = [] + for j in range(SEGMENTS): + midpoint = SCALE + (2 * j + 1) * HALF_STEP + logs.append(round_away(mp.log(mp.mpf(midpoint) / SCALE) * SCALE)) + reciprocals.append(round_away(mp.mpf(Q42 << RECIP_SHIFT) / midpoint)) + + output = f"""// @generated by scripts/generate_ln_lut.py; do not edit manually. +// Midpoints partition normalized mantissas [1, 2) into {SEGMENTS} equal bins. + +pub(crate) const LN_LUT_SEGMENTS: usize = {SEGMENTS}; +pub(crate) const LN_LUT_STEP: u128 = {STEP}; +pub(crate) const LN_LUT_HALF_STEP: u128 = {HALF_STEP}; + +""" + output += render("LN_LUT_MID_LOG", "i64", logs, "LN_LUT_SEGMENTS") + output += "\n" + output += render("LN_Q42_RECIP_G32", "i64", reciprocals, "LN_LUT_SEGMENTS") + Path(__file__).resolve().parents[1].joinpath("src/ln_lut.rs").write_text(output) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_nig_k1_coeffs.py b/scripts/generate_nig_k1_coeffs.py new file mode 100644 index 0000000..04e0852 --- /dev/null +++ b/scripts/generate_nig_k1_coeffs.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Regenerate the scaled-K1 Chebyshev coefficients embedded in src/nig.rs.""" + +from __future__ import annotations + +import hashlib +import json + +import numpy as np +from numpy.polynomial import Chebyshev, Polynomial +from scipy.special import k1e + + +SCALE = 10**12 +SAMPLES = 200_000 + + +def fit_power(x: np.ndarray, y: np.ndarray, degree: int) -> list[int]: + polynomial = Chebyshev.fit(x, y, degree, domain=[-1, 1]).convert(kind=Polynomial) + return [round(float(value) * SCALE) for value in polynomial.coef] + + +def fit_unit_interval(x: np.ndarray, y: np.ndarray, degree: int) -> list[int]: + polynomial = Chebyshev.fit(x, y, degree, domain=[0, 1]).convert(kind=Polynomial) + return [round(float(value) * SCALE) for value in polynomial.coef] + + +def main() -> None: + chebyshev_nodes = np.cos((np.arange(SAMPLES) + 0.5) / SAMPLES * np.pi) + edges = [0.0, 2**-8] + while edges[-1] < 1.0: + edges.append(edges[-1] * 2) + + small = [] + for low, high in zip(edges[:-1], edges[1:]): + z = (chebyshev_nodes + 1) / 2 * (high - low) + low + target = z * k1e(z) + target[z < 1e-15] = 1.0 + small.append(fit_power(chebyshev_nodes, target, 6)) + + reciprocal = (chebyshev_nodes + 1) / 2 + z = np.divide(1.0, reciprocal, out=np.full_like(reciprocal, np.inf), where=reciprocal != 0) + target = np.sqrt(z) * k1e(z) + large = fit_unit_interval(reciprocal, target, 8) + payload = {"scale": SCALE, "samples": SAMPLES, "small": small, "large": large} + canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + payload["sha256"] = hashlib.sha256(canonical).hexdigest() + print(json.dumps(payload, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_norm_cdf_coeffs.py b/scripts/generate_norm_cdf_coeffs.py new file mode 100644 index 0000000..dceba5e --- /dev/null +++ b/scripts/generate_norm_cdf_coeffs.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""Generate the guarded fixed-point minimax-style normal-CDF coefficients. + +The body is split into half-sigma intervals so each polynomial remains both +small and cheap on SBF. Coefficients carry 23 binary guard bits; Horner's +normalized argument is Q44. The four tail pieces approximate Phi(-x) +directly, avoiding exp and division in the standalone CDF path. +""" + +from pathlib import Path + +import mpmath + + +mpmath.mp.dps = 80 + +SCALE = 10**12 +T_Q = 44 +COEFF_GUARD_Q = 23 +TAIL_EVAL_EXTRA_Q = 16 +TAIL_STORAGE_LEN = 7 + +BODY_PIECES = [ + ("NORM_CDF_0_05_Q23", mpmath.mpf("0.0"), mpmath.mpf("0.5"), 8), + ("NORM_CDF_05_10_Q23", mpmath.mpf("0.5"), mpmath.mpf("1.0"), 8), + ("NORM_CDF_10_15_Q23", mpmath.mpf("1.0"), mpmath.mpf("1.5"), 8), + ("NORM_CDF_15_20_Q23", mpmath.mpf("1.5"), mpmath.mpf("2.0"), 8), + ("NORM_CDF_20_25_Q23", mpmath.mpf("2.0"), mpmath.mpf("2.5"), 8), + ("NORM_CDF_25_30_Q23", mpmath.mpf("2.5"), mpmath.mpf("3.0"), 8), + ("NORM_CDF_30_35_Q23", mpmath.mpf("3.0"), mpmath.mpf("3.5"), 8), + ("NORM_CDF_35_40_Q23", mpmath.mpf("3.5"), mpmath.mpf("4.0"), 7), + ("NORM_CDF_40_45_Q23", mpmath.mpf("4.0"), mpmath.mpf("4.5"), 7), + ("NORM_CDF_45_50_Q23", mpmath.mpf("4.5"), mpmath.mpf("5.0"), 7), +] + +TAIL_PIECES = [ + ("NORM_TAIL_50_55_Q23", mpmath.mpf("5.0"), mpmath.mpf("5.5"), 6), + ("NORM_TAIL_55_60_Q23", mpmath.mpf("5.5"), mpmath.mpf("6.0"), 5), + ("NORM_TAIL_60_65_Q23", mpmath.mpf("6.0"), mpmath.mpf("6.5"), 4), + ("NORM_TAIL_65_70_Q23", mpmath.mpf("6.5"), mpmath.mpf("7.0"), 3), +] + +# Sub-raw-unit coefficient refinements align independently fitted intervals at +# raw-input resolution. They are expressed in guarded coefficient units +# (2^-23 raw output units). A dense 250,001-point sweep of every body piece +# keeps the final error within 2 ULP after these two seam constraints. +BODY_COEFFICIENT_ADJUSTMENTS = { + # These are the Q23 equivalents of the fitted Q20 seam refinements. Their + # real-valued magnitude is unchanged; only the coefficient guard scale grew. + "NORM_CDF_10_15_Q23": {1: -7_200_000}, + "NORM_CDF_45_50_Q23": {0: -1_101_632}, +} + + +def fit(a, b, degree, tail=False): + """Return ascending power-basis coefficients for t in [-1, 1].""" + midpoint = (a + b) / 2 + half_width = (b - a) / 2 + fn = ( + (lambda t: mpmath.ncdf(-(midpoint + half_width * t)) * SCALE) + if tail + else (lambda t: mpmath.ncdf(midpoint + half_width * t) * SCALE) + ) + descending = mpmath.chebyfit(fn, [-1, 1], degree + 1) + guard = 1 << COEFF_GUARD_Q + return [int(mpmath.nint(value * guard)) for value in reversed(descending)] + + +def rust_int(value): + sign = "-" if value < 0 else "" + return f"{sign}{abs(value):_d}" + + +def render_array(name, coefficients): + lines = ["#[rustfmt::skip]", f"pub(crate) const {name}: [i64; {len(coefficients)}] = ["] + lines.extend(f" {rust_int(value)}," for value in coefficients) + lines.append("];") + return "\n".join(lines) + + +def tail_half_raw_cutoff(): + # Beyond this input Phi(-x) rounds from one raw unit to zero. Verify both + # neighboring raw inputs so the emitted integer does not depend on a + # presentation-level rounding choice. + root = mpmath.findroot( + lambda x: mpmath.ncdf(-x) * SCALE - mpmath.mpf("0.5"), + (mpmath.mpf("7.0"), mpmath.mpf("7.3")), + ) + cutoff = int(mpmath.floor(root * SCALE)) + at_cutoff = mpmath.ncdf(-mpmath.mpf(cutoff) / SCALE) * SCALE + after_cutoff = mpmath.ncdf(-mpmath.mpf(cutoff + 1) / SCALE) * SCALE + assert at_cutoff > mpmath.mpf("0.5") + assert after_cutoff < mpmath.mpf("0.5") + return cutoff + + +def render(): + sections = [ + "// @generated by scripts/generate_norm_cdf_coeffs.py; do not edit manually.", + "// Coefficients carry 23 binary guard bits and use a Q44 normalized argument.", + "", + f"pub(crate) const CDF_T_Q: u32 = {T_Q};", + f"pub(crate) const CDF_COEFF_GUARD_Q: u32 = {COEFF_GUARD_Q};", + f"pub(crate) const CDF_TAIL_EVAL_EXTRA_Q: u32 = {TAIL_EVAL_EXTRA_Q};", + "", + ] + for name, a, b, degree in BODY_PIECES: + coefficients = fit(a, b, degree) + for index, adjustment in BODY_COEFFICIENT_ADJUSTMENTS.get(name, {}).items(): + coefficients[index] += adjustment + sections.extend((render_array(name, coefficients), "")) + for name, a, b, degree in TAIL_PIECES: + coefficients = fit(a, b, degree, tail=True) + coefficients.extend([0] * (TAIL_STORAGE_LEN - len(coefficients))) + sections.extend((render_array(name, coefficients), "")) + sections.append( + "pub(crate) const NORM_TAIL_HALF_RAW_CUTOFF: i128 = " + f"{rust_int(tail_half_raw_cutoff())};" + ) + return "\n".join(sections) + "\n" + + +def main(): + destination = Path(__file__).resolve().parents[1] / "src" / "norm_cdf_coeffs.rs" + destination.write_text(render()) + print(f"wrote {destination}") + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_production_vectors.py b/scripts/generate_production_vectors.py index 1e00b2a..2c0ecf0 100644 --- a/scripts/generate_production_vectors.py +++ b/scripts/generate_production_vectors.py @@ -79,18 +79,29 @@ def stratified_2d(buckets_x, buckets_y, n_per_cell): def crosscheck(vectors, scipy_fn, key_in, key_expected, tol, label): """Cross-check 1000 random samples against a scipy reference.""" samples = random.sample(vectors, min(1000, len(vectors))) - errors = 0 + mismatches = 0 + exceptions = 0 + first_exception = None for v in samples: try: scipy_ref = scipy_fn(v) mpmath_ref = int(v[key_expected]) if abs(scipy_ref - mpmath_ref) > tol: - errors += 1 - except: - pass - status = "OK" if errors == 0 else f"WARNING: {errors} mismatches" + mismatches += 1 + except Exception as exc: + exceptions += 1 + if first_exception is None: + first_exception = repr(exc) + if not samples: + raise RuntimeError(f"{label} cross-check selected zero samples") + status = "OK" if mismatches == 0 and exceptions == 0 else ( + f"FAILED: {mismatches} mismatches, {exceptions} reference errors" + ) print(f" Cross-check vs {label}: {len(samples)} samples, {status}") - return errors + if mismatches or exceptions: + detail = f"; first error: {first_exception}" if first_exception else "" + raise RuntimeError(f"{label} cross-check failed{detail}") + return 0 # ════════════════════════════════════════════════════════════ @@ -158,6 +169,7 @@ def to_fp(v): def gen_prod_ln(): print(" ln_fixed_i ...") + rng = random.Random(0x1A2026) buckets = [ (0.001, 0.01), (0.01, 0.1), @@ -171,21 +183,22 @@ def gen_prod_ln(): n_per = N // len(buckets) vectors = [] - for lo, hi in buckets: + for bucket_index, (lo, hi) in enumerate(buckets): for _ in range(n_per): if lo < 1 and hi > 1: - x_real = random.uniform(lo, hi) + x_real = rng.uniform(lo, hi) else: log_lo = math.log10(lo) log_hi = math.log10(hi) - x_real = 10 ** random.uniform(log_lo, log_hi) + x_real = 10 ** rng.uniform(log_lo, log_hi) x = int(x_real * SCALE) if x <= 0: continue ref = _nint(mpmath.log(mpmath.mpf(x) / SCALE) * SCALE) if ref > I128_MAX or ref < I128_MIN: continue - vectors.append({"x": _to_str(x), "expected": _to_str(ref)}) + vectors.append({"x": _to_str(x), "expected": _to_str(ref), + "category": f"bucket_{bucket_index}_{lo}_{hi}"}) crosscheck(vectors, lambda v: int(round(float(np.log(float(int(v['x'])) / SCALE)) * SCALE)), @@ -197,6 +210,100 @@ def gen_prod_ln(): "n": len(vectors)}) +def gen_prod_ln_1p(): + print(" ln_1p_fixed ...") + vectors = [] + + def add(x, category): + if x <= -SCALE or x > I128_MAX: + return + ref = _nint(mpmath.log1p(mpmath.mpf(x) / SCALE) * SCALE) + if I128_MIN <= ref <= I128_MAX: + vectors.append({"x": _to_str(x), "expected": _to_str(ref), + "category": category}) + + # Ten equally weighted regimes cover the domain edge, ordinary rates, + # the sub-ULP cancellation region, and large positive values. + for _ in range(N // 10): + one_plus_x_raw = int(10 ** random.uniform(0, 10)) + add(-SCALE + max(1, one_plus_x_raw), "near_domain_edge") + for _ in range(N // 10): + add(random.randint(-990_000_000_000, -500_000_000_000), "negative_large") + for _ in range(N // 10): + add(random.randint(-500_000_000_000, -10_000_000_000), "negative_mid") + for _ in range(N // 10): + add(random.randint(-10_000_000_000, -1_000_000), "negative_small") + for _ in range(N // 10): + add(random.randint(-999_999, -1), "near_zero_negative") + for _ in range(N // 10): + add(random.randint(0, 999_999), "near_zero_nonnegative") + for _ in range(N // 10): + add(random.randint(1_000_000, 10_000_000_000), "positive_small") + for _ in range(N // 10): + add(random.randint(10_000_000_000, SCALE), "positive_mid") + for _ in range(N // 10): + real = 10 ** random.uniform(0, 6) + add(int(real * SCALE), "positive_large") + for _ in range(N // 10): + real = 10 ** random.uniform(6, 15) + add(int(real * SCALE), "positive_huge") + + # IEEE f64 cannot preserve raw-unit inputs immediately above x=-1 or at + # very large magnitudes, so use the ordinary-rate subset for the secondary + # NumPy cross-check. mpmath remains the primary reference for every row. + numpy_safe = [v for v in vectors + if v["category"] not in ("near_domain_edge", "positive_huge")] + crosscheck( + numpy_safe, + lambda v: int(round(float(np.log1p(float(int(v['x'])) / SCALE)) * SCALE)), + 'x', 'expected', 100, 'numpy.log1p') + + save_vectors("prod_ln_1p_vectors.json", vectors, + {"suite": "production", "function": "ln_1p_fixed", + "domain": "10 equal regimes over (-1, 1e15], including raw-unit near-zero inputs", + "reference": "mpmath.log1p at 50 decimal digits; 1,000-sample numpy.log1p cross-check", + "n": len(vectors)}) + + +def gen_prod_expm1(): + print(" expm1_fixed ...") + vectors = [] + + def add(x, category): + ref = _nint(mpmath.expm1(mpmath.mpf(x) / SCALE) * SCALE) + if I128_MIN <= ref <= I128_MAX: + vectors.append({"x": _to_str(x), "expected": _to_str(ref), + "category": category}) + + regimes = ( + (-40 * SCALE, -20 * SCALE, "negative_saturation"), + (-20 * SCALE, -2 * SCALE, "negative_large"), + (-2 * SCALE, -SCALE // 2, "negative_mid"), + (-SCALE // 2, -1_000_000, "negative_small"), + (-999_999, -1, "near_zero_negative"), + (0, 999_999, "near_zero_nonnegative"), + (1_000_000, SCALE // 2, "positive_small"), + (SCALE // 2, 2 * SCALE, "positive_mid"), + (2 * SCALE, 20 * SCALE, "positive_large"), + (20 * SCALE, 40 * SCALE - 1, "near_overflow"), + ) + for lo, hi, category in regimes: + for _ in range(N // len(regimes)): + add(random.randint(lo, hi), category) + + numpy_safe = [v for v in vectors if abs(int(v["x"])) <= 2 * SCALE] + crosscheck( + numpy_safe, + lambda v: int(round(float(np.expm1(float(int(v['x'])) / SCALE)) * SCALE)), + 'x', 'expected', 2, 'numpy.expm1') + + save_vectors("prod_expm1_vectors.json", vectors, + {"suite": "production", "function": "expm1_fixed", + "domain": "10 equal regimes over [-40,40), including raw near-zero inputs", + "reference": "mpmath.expm1 at 50 decimal digits; 1,000-sample numpy.expm1 cross-check", + "n": len(vectors)}) + + def gen_prod_ln_hp(): print(" ln_fixed_hp ...") buckets = [ @@ -242,25 +349,35 @@ def gen_prod_exp(): (3, 10), (10, 20), ] - n_per = N // len(buckets) - vectors = [] - for lo, hi in buckets: - for _ in range(n_per): + quotient, remainder = divmod(N, len(buckets)) + for bucket_index, (lo, hi) in enumerate(buckets): + # Distribute the remainder instead of silently emitting 99,999 rows + # when N is not divisible by the nine production regimes. + count = quotient + (1 if bucket_index < remainder else 0) + for _ in range(count): x_real = random.uniform(lo, hi) x = int(x_real * SCALE) ref = _nint(mpmath.exp(mpmath.mpf(x) / SCALE) * SCALE) if ref > I128_MAX or ref <= 0: continue - vectors.append({"x": _to_str(x), "expected": _to_str(ref)}) - - crosscheck(vectors, + vectors.append({"x": _to_str(x), "expected": _to_str(ref), + "category": f"bucket_{bucket_index}_{lo}_{hi}"}) + + # At large positive x, converting the scaled result to binary64 loses far + # more than 1,000 raw decimal units even when both references agree. Keep + # the independent NumPy check in the ordinary |x| <= 3 domain where its + # integer conversion has adequate resolution; mpmath is authoritative for + # all 100,000 rows. + numpy_safe = [v for v in vectors if abs(int(v["x"])) <= 3 * SCALE] + crosscheck(numpy_safe, lambda v: int(round(float(np.exp(float(int(v['x'])) / SCALE)) * SCALE)), 'x', 'expected', 1000, 'numpy') save_vectors("prod_exp_vectors.json", vectors, {"suite": "production", "function": "exp_fixed_i", "domain": "[-20, 20] stratified 9 buckets, oversampled near 0", + "reference": "mpmath.exp at 50 decimal digits; 1,000-sample NumPy cross-check over |x| <= 3", "n": len(vectors)}) @@ -378,6 +495,7 @@ def gen_prod_cos(): def gen_prod_norm_cdf(): print(" norm_cdf_poly ...") + rng = random.Random(0xCDF2026) buckets = [ (-6, -3), (-3, -2), @@ -391,12 +509,13 @@ def gen_prod_norm_cdf(): n_per = N // len(buckets) vectors = [] - for lo, hi in buckets: + for bucket_index, (lo, hi) in enumerate(buckets): for _ in range(n_per): - x_real = random.uniform(lo, hi) + x_real = rng.uniform(lo, hi) x = int(x_real * SCALE) ref = _nint(mpmath.ncdf(mpmath.mpf(x) / SCALE) * SCALE) - vectors.append({"x": _to_str(x), "expected": _to_str(ref)}) + vectors.append({"x": _to_str(x), "expected": _to_str(ref), + "category": f"bucket_{bucket_index}_{lo}_{hi}"}) crosscheck(vectors, lambda v: int(round(float(scipy.stats.norm.cdf(float(int(v['x'])) / SCALE)) * SCALE)), @@ -979,6 +1098,22 @@ def gen_prod_checked_mul_div(): print("SolMath Production Validation Vector Generator") print(f"N = {N} vectors per function, stratified domains\n") + if "--ln-1p-only" in sys.argv: + gen_prod_ln_1p() + sys.exit(0) + if "--expm1-only" in sys.argv: + gen_prod_expm1() + sys.exit(0) + if "--exp-only" in sys.argv: + gen_prod_exp() + sys.exit(0) + if "--ln-only" in sys.argv: + gen_prod_ln() + sys.exit(0) + if "--norm-cdf-only" in sys.argv: + gen_prod_norm_cdf() + sys.exit(0) + # Arithmetic gen_prod_fp_mul() gen_prod_fp_mul_i() @@ -988,6 +1123,8 @@ def gen_prod_checked_mul_div(): # Transcendentals gen_prod_ln() + gen_prod_ln_1p() + gen_prod_expm1() gen_prod_ln_hp() gen_prod_exp() gen_prod_exp_hp() diff --git a/scripts/generate_references.py b/scripts/generate_references.py index 3932dc1..abb074b 100644 --- a/scripts/generate_references.py +++ b/scripts/generate_references.py @@ -1,318 +1,161 @@ #!/usr/bin/env python3 -"""Generate 100K QuantLib reference vectors each for SABR and Heston. +"""Generate the repository-only QuantLib SABR corpus and Rust subset.""" -Outputs: - test_data/sabr_vectors.json (100K) - test_data/heston_vectors.json (100K) - test_data/sabr_reference_tests.rs (subset for cargo test) - test_data/heston_reference_tests.rs (subset for cargo test) -""" +from __future__ import annotations -import QuantLib as ql -import numpy as np +import argparse import json -import os -import sys +import pathlib + +import numpy as np +import QuantLib as ql + +ROOT = pathlib.Path(__file__).resolve().parents[1] SCALE = 1_000_000_000_000 -def to_fp(x): - return int(round(x * SCALE)) -# ============================================================ -# SABR — 100K vectors -# ============================================================ +def to_fp(value: float) -> int: + return int(round(value * SCALE)) + + +def row( + forward: float, + strike: float, + maturity: float, + alpha: float, + beta: float, + rho: float, + nu: float, + vol: float, +) -> dict[str, float | int]: + return { + "F": forward, + "K": round(strike, 10), + "T": maturity, + "alpha": alpha, + "beta": beta, + "rho": rho, + "nu": nu, + "vol": vol, + "F_fp": to_fp(forward), + "K_fp": to_fp(strike), + "T_fp": to_fp(maturity), + "alpha_fp": to_fp(alpha), + "beta_fp": to_fp(beta), + "rho_fp": to_fp(rho), + "nu_fp": to_fp(nu), + "vol_fp": to_fp(vol), + } -def sabr_vectors(target=100_000): - rng = np.random.default_rng(42) - results = [] - # Systematic grid: 20 alpha × 5 beta × 11 rho × 7 nu × 7 T × 11 K/F = 59,290 - # Plus random fill to 100K +def sabr_vectors(target: int) -> list[dict[str, float | int]]: + if target <= 0: + raise ValueError("target must be positive") + rng = np.random.default_rng(42) + results: list[dict[str, float | int]] = [] + forward = 100.0 alphas = np.linspace(0.02, 0.50, 20) - betas = [0.0, 0.25, 0.5, 0.75, 1.0] - rhos = np.linspace(-0.90, 0.50, 11) - nus = [0.05, 0.10, 0.20, 0.30, 0.40, 0.60, 0.80] - Ts = [0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0] + betas = [0.0, 0.25, 0.5, 0.75, 1.0] + rhos = np.linspace(-0.90, 0.50, 11) + nus = [0.05, 0.10, 0.20, 0.30, 0.40, 0.60, 0.80] + maturities = [0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0] ratios = [0.50, 0.70, 0.80, 0.90, 0.95, 1.00, 1.05, 1.10, 1.20, 1.50, 2.00] - F = 100.0 - - count = 0 - total_grid = len(alphas) * len(betas) * len(rhos) * len(nus) * len(Ts) * len(ratios) - print(f" SABR grid size: {total_grid}") for alpha in alphas: for beta in betas: for rho in rhos: for nu in nus: - for T in Ts: + for maturity in maturities: for ratio in ratios: - K = F * ratio - if K <= 0: - continue + strike = forward * ratio try: - vol = ql.sabrVolatility(K, F, T, alpha, beta, nu, rho) - if vol <= 0 or vol > 5.0 or np.isnan(vol) or np.isinf(vol): - continue - results.append(_sabr_row(F, K, T, alpha, beta, rho, nu, vol)) - count += 1 - if count >= target: + vol = ql.sabrVolatility( + strike, forward, maturity, alpha, beta, nu, rho + ) + except RuntimeError: + continue + if np.isfinite(vol) and 0.0 < vol <= 5.0: + results.append( + row( + forward, + strike, + maturity, + float(alpha), + beta, + float(rho), + nu, + float(vol), + ) + ) + if len(results) == target: return results - except Exception: - pass - if count % 10000 == 0 and count > 0: - print(f" SABR grid: {count}/{target}") - # Random fill for remaining - print(f" SABR grid produced {count}, filling to {target} with random...") - while count < target: + while len(results) < target: alpha = rng.uniform(0.01, 0.60) beta = rng.uniform(0.0, 1.0) rho = rng.uniform(-0.95, 0.60) nu = rng.uniform(0.01, 1.0) - T = rng.choice([0.1, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0]) - ratio = rng.uniform(0.5, 2.0) - K = F * ratio + maturity = float(rng.choice(maturities)) + strike = forward * rng.uniform(0.5, 2.0) try: - vol = ql.sabrVolatility(K, F, T, alpha, beta, nu, rho) - if vol <= 0 or vol > 5.0 or np.isnan(vol) or np.isinf(vol): - continue - results.append(_sabr_row(F, K, T, alpha, beta, rho, nu, vol)) - count += 1 - if count % 10000 == 0: - print(f" SABR random: {count}/{target}") - except Exception: - pass - - return results - -def _sabr_row(F, K, T, alpha, beta, rho, nu, vol): - return { - "F": F, "K": round(K, 10), "T": T, - "alpha": alpha, "beta": beta, "rho": rho, "nu": nu, - "vol": vol, - "F_fp": to_fp(F), "K_fp": to_fp(K), "T_fp": to_fp(T), - "alpha_fp": to_fp(alpha), "beta_fp": to_fp(beta), - "rho_fp": to_fp(rho), "nu_fp": to_fp(nu), - "vol_fp": to_fp(vol), - } - -# ============================================================ -# Heston — 100K vectors -# ============================================================ - -def heston_vectors(target=100_000): - rng = np.random.default_rng(123) - results = [] - count = 0 - - # Systematic grid first - Ss = [100.0] - rs = [0.0, 0.02, 0.05, 0.10] - v0s = [0.01, 0.04, 0.09, 0.16, 0.25] - kappas = [0.5, 1.0, 2.0, 3.0, 5.0] - thetas = [0.01, 0.04, 0.09, 0.16] - xis = [0.1, 0.2, 0.3, 0.5, 0.8] - rhos_h = [-0.9, -0.7, -0.5, -0.3, 0.0] - Ts = [0.1, 0.25, 0.5, 1.0, 2.0] - Ks = [80.0, 85.0, 90.0, 95.0, 100.0, 105.0, 110.0, 115.0, 120.0] - - total_grid = (len(Ss) * len(rs) * len(v0s) * len(kappas) * len(thetas) - * len(xis) * len(rhos_h) * len(Ts) * len(Ks)) - print(f" Heston grid size: {total_grid}") - - for S in Ss: - for r in rs: - for v0 in v0s: - for kappa in kappas: - for theta in thetas: - for xi in xis: - for rho in rhos_h: - for T in Ts: - for K in Ks: - row = _heston_price(S, K, T, r, v0, kappa, theta, xi, rho) - if row: - results.append(row) - count += 1 - if count >= target: - return results - if count % 10000 == 0 and count > 0: - print(f" Heston grid: {count}/{target}") - - # Random fill - print(f" Heston grid produced {count}, filling to {target} with random...") - while count < target: - S = 100.0 - r = rng.uniform(0.0, 0.12) - v0 = rng.uniform(0.005, 0.30) - kappa = rng.uniform(0.3, 6.0) - theta = rng.uniform(0.005, 0.25) - xi = rng.uniform(0.05, 1.0) - rho = rng.uniform(-0.95, 0.10) - T = rng.choice([0.1, 0.25, 0.5, 1.0, 2.0]) - K = rng.choice([80.0, 85.0, 90.0, 95.0, 100.0, 105.0, 110.0, 115.0, 120.0]) - row = _heston_price(S, K, T, r, v0, kappa, theta, xi, rho) - if row: - results.append(row) - count += 1 - if count % 10000 == 0: - print(f" Heston random: {count}/{target}") - + vol = ql.sabrVolatility(strike, forward, maturity, alpha, beta, nu, rho) + except RuntimeError: + continue + if np.isfinite(vol) and 0.0 < vol <= 5.0: + results.append( + row(forward, strike, maturity, alpha, beta, rho, nu, float(vol)) + ) return results -def _heston_price(S, K, T, r, v0, kappa, theta, xi, rho): - try: - today = ql.Date(1, 1, 2025) - ql.Settings.instance().evaluationDate = today - mat = today + ql.Period(max(1, int(T * 365.25)), ql.Days) - dc = ql.Actual365Fixed() - spot = ql.QuoteHandle(ql.SimpleQuote(S)) - rate_ts = ql.YieldTermStructureHandle(ql.FlatForward(today, r, dc)) - div_ts = ql.YieldTermStructureHandle(ql.FlatForward(today, 0.0, dc)) - proc = ql.HestonProcess(rate_ts, div_ts, spot, v0, kappa, theta, xi, rho) - model = ql.HestonModel(proc) - engine = ql.AnalyticHestonEngine(model, 1e-12, 5000) - - call_opt = ql.VanillaOption( - ql.PlainVanillaPayoff(ql.Option.Call, K), ql.EuropeanExercise(mat)) - call_opt.setPricingEngine(engine) - cp = call_opt.NPV() - - put_opt = ql.VanillaOption( - ql.PlainVanillaPayoff(ql.Option.Put, K), ql.EuropeanExercise(mat)) - put_opt.setPricingEngine(engine) - pp = put_opt.NPV() - if np.isnan(cp) or np.isnan(pp) or cp < 0 or pp < 0: - return None +def render_rust(cases: list[dict[str, float | int]], count: int) -> str: + step = max(1, len(cases) // count) + subset = cases[::step][:count] + output = [ + "// Auto-generated from QuantLib. Do not edit.", + f"// {len(subset)} of {len(cases)} vectors (every {step}th)", + "", + "#[cfg(test)]", + "mod quantlib_sabr {", + " use solmath::sabr_implied_vol;", + "", + ] + for index, case in enumerate(subset): + expected = int(case["vol_fp"]) + output.extend( + [ + " #[test]", + f" fn ql_sabr_{index:04d}() {{", + " let vol = sabr_implied_vol(", + f" {case['F_fp']}u128, {case['K_fp']}u128, {case['T_fp']}u128,", + f" {case['alpha_fp']}u128, {case['beta_fp']}u128, {case['rho_fp']}i128, {case['nu_fp']}u128,", + " ).unwrap();", + f" let expected = {expected}u128;", + " let tolerance = expected / 200;", + " assert!(vol.abs_diff(expected) <= tolerance);", + " }", + "", + ] + ) + output.append("}") + return "\n".join(output) + "\n" + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--target", type=int, default=100_000) + parser.add_argument("--rust-subset", type=int, default=500) + args = parser.parse_args() + cases = sabr_vectors(args.target) + data_dir = ROOT / "test_data" + data_dir.mkdir(exist_ok=True) + (data_dir / "sabr_vectors.json").write_text(json.dumps(cases)) + (data_dir / "sabr_reference_tests.rs").write_text( + render_rust(cases, args.rust_subset) + ) + print(f"generated {len(cases):,} SABR vectors") - return { - "S": S, "K": K, "T": T, "r": r, - "v0": v0, "kappa": kappa, "theta": theta, "xi": xi, "rho": rho, - "call": cp, "put": pp, - "S_fp": to_fp(S), "K_fp": to_fp(K), "T_fp": to_fp(T), "r_fp": to_fp(r), - "v0_fp": to_fp(v0), "kappa_fp": to_fp(kappa), "theta_fp": to_fp(theta), - "xi_fp": to_fp(xi), "rho_fp": to_fp(rho), - "call_fp": to_fp(cp), "put_fp": to_fp(pp), - } - except Exception: - return None - -# ============================================================ -# Write Rust test files (subset for cargo test — 500 each) -# ============================================================ - -def write_sabr_rs(cases, path, n=500): - """Write n evenly-spaced cases as Rust tests.""" - step = max(1, len(cases) // n) - subset = cases[::step][:n] - with open(path, "w") as f: - f.write("// Auto-generated from QuantLib. Do not edit.\n") - f.write(f"// {len(subset)} of {len(cases)} vectors (every {step}th)\n\n") - f.write("#[cfg(test)]\nmod quantlib_sabr {\n") - f.write(" use crate::sabr::sabr_implied_vol;\n\n") - for i, c in enumerate(subset): - f.write(f" #[test]\n") - f.write(f" fn ql_sabr_{i:04d}() {{\n") - f.write(f" // F={c['F']}, K={c['K']:.6f}, T={c['T']}, vol={c['vol']:.10f}\n") - f.write(f" let vol = sabr_implied_vol(\n") - f.write(f" {c['F_fp']}u128, {c['K_fp']}u128, {c['T_fp']}u128,\n") - f.write(f" {c['alpha_fp']}u128, {c['beta_fp']}u128, {c['rho_fp']}i128, {c['nu_fp']}u128,\n") - f.write(f" ).unwrap();\n") - f.write(f" let expected = {c['vol_fp']}u128;\n") - f.write(f" let tol = expected / 200; // 0.5%\n") - f.write(f" let diff = if vol > expected {{ vol - expected }} else {{ expected - vol }};\n") - f.write(f" assert!(diff <= tol,\n") - f.write(f' "SABR#{i}: vol={{}} exp={{}} diff={{}} tol={{}}", vol, expected, diff, tol);\n') - f.write(f" }}\n\n") - f.write("}\n") - -def write_heston_rs(cases, path, n=500): - step = max(1, len(cases) // n) - subset = cases[::step][:n] - with open(path, "w") as f: - f.write("// Auto-generated from QuantLib. Do not edit.\n") - f.write(f"// {len(subset)} of {len(cases)} vectors (every {step}th)\n\n") - f.write("#[cfg(test)]\nmod quantlib_heston {\n") - f.write(" use crate::heston::heston_price;\n\n") - for i, c in enumerate(subset): - xi_val = c['xi'] - rho_abs = abs(c['rho']) - moneyness = abs(c['S'] / c['K'] - 1.0) - kappa_val = c['kappa'] - v0_val = c['v0'] - theta_val = c['theta'] - base_tol = 0.05 - if xi_val > 0.3: - base_tol += (xi_val - 0.3) * 1.0 - if rho_abs > 0.7: - base_tol += (rho_abs - 0.7) * 1.0 - if c['T'] <= 0.25 and c['K'] != c['S']: - base_tol = max(base_tol, 0.25) - if moneyness > 0.1: - base_tol += moneyness * 0.3 - # High kappa: fast mean reversion amplifies approximation error - v0_theta_gap = abs(v0_val - theta_val) - if kappa_val >= 3.0: - base_tol = max(base_tol, kappa_val * 0.06) - if kappa_val >= 2.0 and v0_theta_gap > 0.01: - base_tol = max(base_tol, v0_theta_gap * kappa_val * 0.5) - # BS path (low xi²T): cir_rms_vol approximation - xi_sq_t = xi_val * xi_val * c['T'] - if xi_sq_t < 0.02: - base_tol = max(base_tol, 0.10 + v0_theta_gap * 3.0 + kappa_val * 0.15) - tol_fp = to_fp(base_tol) - - f.write(f" #[test]\n") - f.write(f" fn ql_heston_{i:04d}() {{\n") - f.write(f" // S={c['S']}, K={c['K']}, T={c['T']}, r={c['r']}\n") - f.write(f" // v0={c['v0']}, kappa={c['kappa']}, theta={c['theta']}, xi={c['xi']}, rho={c['rho']}\n") - f.write(f" let (call, put) = heston_price(\n") - f.write(f" {c['S_fp']}u128, {c['K_fp']}u128, {c['r_fp']}u128, {c['T_fp']}u128,\n") - f.write(f" {c['v0_fp']}u128, {c['kappa_fp']}u128, {c['theta_fp']}u128, {c['xi_fp']}u128,\n") - f.write(f" {c['rho_fp']}i128,\n") - f.write(f" ).unwrap();\n") - f.write(f" let exp_call = {c['call_fp']}u128;\n") - f.write(f" let exp_put = {c['put_fp']}u128;\n") - f.write(f" let tol = {tol_fp}u128; // ${base_tol:.2f}\n") - f.write(f" let dc = if call > exp_call {{ call - exp_call }} else {{ exp_call - call }};\n") - f.write(f" let dp = if put > exp_put {{ put - exp_put }} else {{ exp_put - put }};\n") - f.write(f" assert!(dc <= tol,\n") - f.write(f' "Heston#{i} call: got={{}} exp={{}} diff={{}}", call, exp_call, dc);\n') - f.write(f" assert!(dp <= tol,\n") - f.write(f' "Heston#{i} put: got={{}} exp={{}} diff={{}}", put, exp_put, dp);\n') - f.write(f" }}\n\n") - f.write("}\n") - -# ============================================================ if __name__ == "__main__": - os.makedirs("test_data", exist_ok=True) - - target = 100_000 - if len(sys.argv) > 1: - target = int(sys.argv[1]) - - print(f"Generating {target} SABR vectors...") - sabr = sabr_vectors(target) - print(f" {len(sabr)} SABR vectors generated") - - print(f"Generating {target} Heston vectors...") - heston = heston_vectors(target) - print(f" {len(heston)} Heston vectors generated") - - print("Writing JSON...") - with open("test_data/sabr_vectors.json", "w") as f: - json.dump(sabr, f) - with open("test_data/heston_vectors.json", "w") as f: - json.dump(heston, f) - - print("Writing Rust tests (500 each)...") - write_sabr_rs(sabr, "test_data/sabr_reference_tests.rs", 500) - write_heston_rs(heston, "test_data/heston_reference_tests.rs", 500) - - sabr_mb = os.path.getsize("test_data/sabr_vectors.json") / 1e6 - heston_mb = os.path.getsize("test_data/heston_vectors.json") / 1e6 - print(f"\nSABR: {len(sabr):,} vectors ({sabr_mb:.1f} MB)") - print(f"Heston: {len(heston):,} vectors ({heston_mb:.1f} MB)") - print(f"Rust: 500 + 500 = 1,000 inline tests") + main() diff --git a/scripts/measure_downstream_gaps.rs b/scripts/measure_downstream_gaps.rs new file mode 100644 index 0000000..69e30a4 --- /dev/null +++ b/scripts/measure_downstream_gaps.rs @@ -0,0 +1,496 @@ +//! Revalidate the retained downstream gaps after ln/CDF implementation changes. +//! +//! This runner is intentionally dependency-free. Build the library with +//! `full,table-gen`, compile this file with `rustc`, then pass the BVN, Phi2, +//! SABR, Heston, NIG-i128, and NIG-i64 JSON paths in that order. + +use solmath::{ + black_scholes_price_hp, bvn_cdf, bvn_cdf_hp, heston_price, nig_call_64, nig_call_price, + nig_put_64, sabr_greeks, sabr_implied_vol, sabr_precompute, sabr_price, sabr_vol_at, + Phi2DenseTable, Phi2Table, SolMathError, PHI2_DENSE_GRID_SIZE, PHI2_GRID_SIZE, SCALE, +}; +use std::{collections::BTreeMap, env, fs, time::Instant}; + +#[derive(Default)] +struct Stats { + errors: Vec, + failures: BTreeMap, +} + +impl Stats { + fn accept(&mut self, actual: u128, expected: u128) { + self.errors.push(actual.abs_diff(expected)); + } + + fn accept_i(&mut self, actual: i128, expected: i128) { + self.errors.push(actual.abs_diff(expected)); + } + + fn fail(&mut self, error: SolMathError) { + *self.failures.entry(format!("{error:?}")).or_default() += 1; + } + + fn failure_count(&self) -> usize { + self.failures.values().sum() + } + + fn print(mut self, name: &str) { + self.errors.sort_unstable(); + let accepted = self.errors.len(); + let percentile = |numerator: usize, denominator: usize| -> u128 { + if accepted == 0 { + 0 + } else { + self.errors[(accepted - 1) * numerator / denominator] + } + }; + let exact = self.errors.partition_point(|error| *error == 0); + let failures = self.failure_count(); + println!( + "metric={name} vectors={} accepted={accepted} failures={failures} max={} p99={} p95={} median={} exact={} exact_pct={:.6} failure_kinds={:?}", + accepted + failures, + self.errors.last().copied().unwrap_or(0), + percentile(99, 100), + percentile(95, 100), + percentile(1, 2), + exact, + if accepted == 0 { 0.0 } else { exact as f64 * 100.0 / accepted as f64 }, + self.failures, + ); + } +} + +fn numeric_field(object: &str, key: &str) -> Option { + let needle = format!("\"{key}\""); + let key_at = object.find(&needle)?; + let colon = object[key_at + needle.len()..].find(':')? + key_at + needle.len(); + let bytes = object.as_bytes(); + let mut cursor = colon + 1; + while bytes.get(cursor).is_some_and(u8::is_ascii_whitespace) { + cursor += 1; + } + let quoted = bytes.get(cursor) == Some(&b'"'); + if quoted { + cursor += 1; + } + let start = cursor; + if bytes.get(cursor) == Some(&b'-') { + cursor += 1; + } + while bytes.get(cursor).is_some_and(u8::is_ascii_digit) { + cursor += 1; + } + if cursor == start || (cursor == start + 1 && bytes.get(start) == Some(&b'-')) { + return None; + } + object[start..cursor].parse().ok() +} + +fn unsigned_field(object: &str, key: &str) -> Option { + let value = numeric_field(object, key)?; + u128::try_from(value).ok() +} + +fn for_each_object(path: &str, mut visit: impl FnMut(&str)) { + let input = fs::read_to_string(path).unwrap_or_else(|error| panic!("read {path}: {error}")); + let mut cursor = input.find("\"vectors\"").unwrap_or(0); + while let Some(relative_start) = input[cursor..].find('{') { + let start = cursor + relative_start; + let Some(relative_end) = input[start..].find('}') else { + panic!("unterminated object in {path}"); + }; + let end = start + relative_end + 1; + visit(&input[start..end]); + cursor = end; + } +} + +#[derive(Clone, Copy)] +struct BvnRow { + a: i128, + b: i128, + rho: i128, + expected: i128, +} + +fn bvn_tier(rho: i128) -> &'static str { + let absolute = rho.unsigned_abs(); + if absolute <= 900_000_000_000 { + "abs_rho_le_0.90" + } else if absolute <= 950_000_000_000 { + "0.90_lt_abs_rho_le_0.95" + } else if absolute <= 990_000_000_000 { + "0.95_lt_abs_rho_le_0.99" + } else if absolute < SCALE { + "0.99_lt_abs_rho_lt_1" + } else { + "abs_rho_eq_1" + } +} + +fn measure_bvn(path: &str) { + let mut stats: BTreeMap = BTreeMap::new(); + let mut rows = 0usize; + for_each_object(path, |object| { + let Some(a) = numeric_field(object, "a") else { + return; + }; + let row = BvnRow { + a, + b: numeric_field(object, "b").expect("b"), + rho: numeric_field(object, "rho").expect("rho"), + expected: numeric_field(object, "expected").expect("expected"), + }; + rows += 1; + for (function, result) in [ + ("bvn_cdf.GL6", bvn_cdf(row.a, row.b, row.rho)), + ("bvn_cdf_hp.GL20", bvn_cdf_hp(row.a, row.b, row.rho)), + ] { + for tier in ["all", bvn_tier(row.rho)] { + let entry = stats.entry(format!("{function}.{tier}")).or_default(); + match result { + Ok(actual) => entry.accept_i(actual, row.expected), + Err(error) => entry.fail(error), + } + } + } + }); + assert_eq!(rows, 22_500, "BVN corpus size"); + for (name, metric) in stats { + metric.print(&name); + } +} + +fn load_bvn_rows(path: &str) -> Vec { + let mut rows = Vec::new(); + for_each_object(path, |object| { + let Some(a) = numeric_field(object, "a") else { + return; + }; + rows.push(BvnRow { + a, + b: numeric_field(object, "b").expect("b"), + rho: numeric_field(object, "rho").expect("rho"), + expected: numeric_field(object, "expected").expect("expected"), + }); + }); + rows +} + +fn hex(bytes: [u8; 32]) -> String { + let mut output = String::with_capacity(64); + for byte in bytes { + use std::fmt::Write; + write!(output, "{byte:02x}").unwrap(); + } + output +} + +fn measure_phi2(path: &str) { + let rows = load_bvn_rows(path); + assert_eq!(rows.len(), 10_000, "Phi2 corpus size"); + let rhos = [ + -900_000_000_000i128, + -500_000_000_000, + 0, + 500_000_000_000, + 900_000_000_000, + ]; + let mut compatibility = Stats::default(); + let mut dense = Stats::default(); + let mut compatibility_bound_failures = 0usize; + let mut dense_bound_failures = 0usize; + + for rho in rhos { + let started = Instant::now(); + let table = Phi2Table::generate(rho, PHI2_GRID_SIZE).expect("64x64 generation"); + let certificate = table.certify(rho).expect("64x64 certification"); + let evaluator = table + .certified( + &certificate, + certificate.certificate_id(), + certificate.max_abs_error(), + ) + .expect("64x64 guarded evaluator"); + println!( + "phi2_certificate grid=64 rho={rho} node={} interpolation={} reference={} total={} id={} elapsed_ms={}", + certificate.max_node_abs_error(), + certificate.interpolation_abs_error_bound(), + certificate.reference_abs_error_allowance(), + certificate.max_abs_error(), + hex(certificate.certificate_id()), + started.elapsed().as_millis(), + ); + for row in rows.iter().filter(|row| row.rho == rho) { + match evaluator.eval(row.a, row.b) { + Ok(actual) => { + let error = actual.abs_diff(row.expected); + compatibility_bound_failures += + usize::from(error > certificate.max_abs_error() as u128); + compatibility.accept_i(actual, row.expected); + } + Err(error) => compatibility.fail(error), + } + } + + let started = Instant::now(); + let table = + Phi2DenseTable::generate(rho, PHI2_DENSE_GRID_SIZE).expect("129x129 generation"); + let certificate = table.certify(rho).expect("129x129 certification"); + let evaluator = table + .certified( + &certificate, + certificate.certificate_id(), + certificate.max_abs_error(), + ) + .expect("129x129 guarded evaluator"); + println!( + "phi2_certificate grid=129 rho={rho} node={} interpolation={} reference={} total={} id={} elapsed_ms={}", + certificate.max_node_abs_error(), + certificate.interpolation_abs_error_bound(), + certificate.reference_abs_error_allowance(), + certificate.max_abs_error(), + hex(certificate.certificate_id()), + started.elapsed().as_millis(), + ); + for row in rows.iter().filter(|row| row.rho == rho) { + match evaluator.eval(row.a, row.b) { + Ok(actual) => { + let error = actual.abs_diff(row.expected); + dense_bound_failures += + usize::from(error > certificate.max_abs_error() as u128); + dense.accept_i(actual, row.expected); + } + Err(error) => dense.fail(error), + } + } + } + + compatibility.print("Phi2Table.certified.off_grid"); + dense.print("Phi2DenseTable.certified.off_grid"); + println!( + "phi2_bound_failures compatibility={compatibility_bound_failures} dense={dense_bound_failures}" + ); + assert_eq!(compatibility_bound_failures, 0); + assert_eq!(dense_bound_failures, 0); + + // Exercise and cross-check the one-shot generate+certify entrypoints too. + const TEST_RHO: i128 = 750_000_000_000; + let separate = Phi2Table::generate(TEST_RHO, PHI2_GRID_SIZE).unwrap(); + let separate_certificate = separate.certify(TEST_RHO).unwrap(); + let (one_shot, one_shot_certificate) = + Phi2Table::generate_certified(TEST_RHO, PHI2_GRID_SIZE).unwrap(); + assert_eq!(separate.as_array(), one_shot.as_array()); + assert_eq!(separate_certificate, one_shot_certificate); + + let separate = Phi2DenseTable::generate(TEST_RHO, PHI2_DENSE_GRID_SIZE).unwrap(); + let separate_certificate = separate.certify(TEST_RHO).unwrap(); + let (one_shot, one_shot_certificate) = + Phi2DenseTable::generate_certified(TEST_RHO, PHI2_DENSE_GRID_SIZE).unwrap(); + assert_eq!(separate.as_array(), one_shot.as_array()); + assert_eq!(separate_certificate, one_shot_certificate); + println!("phi2_generate_certified_equivalence grid_64=true grid_129=true rho={TEST_RHO}"); +} + +fn measure_sabr(path: &str) { + let mut implied = Stats::default(); + let mut batch = Stats::default(); + let mut batch_vs_direct = Stats::default(); + let mut price_call = Stats::default(); + let mut price_put = Stats::default(); + let mut greeks_call = Stats::default(); + let mut greeks_put = Stats::default(); + let mut rows = 0usize; + + for_each_object(path, |object| { + let Some(forward) = unsigned_field(object, "F_fp") else { + return; + }; + rows += 1; + let strike = unsigned_field(object, "K_fp").unwrap(); + let time = unsigned_field(object, "T_fp").unwrap(); + let alpha = unsigned_field(object, "alpha_fp").unwrap(); + let beta = unsigned_field(object, "beta_fp").unwrap(); + let rho = numeric_field(object, "rho_fp").unwrap(); + let nu = unsigned_field(object, "nu_fp").unwrap(); + let expected_vol = unsigned_field(object, "vol_fp").unwrap(); + + let direct = sabr_implied_vol(forward, strike, time, alpha, beta, rho, nu); + match direct { + Ok(actual) => implied.accept(actual, expected_vol), + Err(error) => implied.fail(error), + } + match sabr_precompute(forward, time, alpha, beta, rho, nu) + .and_then(|precomputed| sabr_vol_at(&precomputed, strike)) + { + Ok(actual) => { + batch.accept(actual, expected_vol); + if let Ok(direct_actual) = direct { + batch_vs_direct.accept(actual, direct_actual); + } + } + Err(error) => { + batch.fail(error); + batch_vs_direct.fail(error); + } + } + + let reference = black_scholes_price_hp(forward, strike, 0, expected_vol, time); + match ( + sabr_price(forward, strike, 0, time, alpha, beta, rho, nu), + reference, + ) { + (Ok((call, put)), Ok((expected_call, expected_put))) => { + price_call.accept(call, expected_call); + price_put.accept(put, expected_put); + } + (Err(error), _) => { + price_call.fail(error); + price_put.fail(error); + } + (_, Err(error)) => { + price_call.fail(error); + price_put.fail(error); + } + } + match ( + sabr_greeks(forward, strike, 0, time, alpha, beta, rho, nu), + reference, + ) { + (Ok(greeks), Ok((expected_call, expected_put))) => { + greeks_call.accept(greeks.call, expected_call); + greeks_put.accept(greeks.put, expected_put); + } + (Err(error), _) => { + greeks_call.fail(error); + greeks_put.fail(error); + } + (_, Err(error)) => { + greeks_call.fail(error); + greeks_put.fail(error); + } + } + }); + assert_eq!(rows, 100_000, "SABR corpus size"); + implied.print("sabr_implied_vol.QuantLib_100K"); + batch.print("sabr_precompute_vol_at.QuantLib_100K"); + batch_vs_direct.print("sabr_batch_vs_direct_100K"); + price_call.print("sabr_price.call_vs_HP_at_QuantLib_vol"); + price_put.print("sabr_price.put_vs_HP_at_QuantLib_vol"); + greeks_call.print("sabr_greeks.call_vs_HP_at_QuantLib_vol"); + greeks_put.print("sabr_greeks.put_vs_HP_at_QuantLib_vol"); +} + +fn measure_heston_fail_closed(path: &str) { + let mut accepted = 0usize; + let mut failures: BTreeMap = BTreeMap::new(); + let mut rows = 0usize; + for_each_object(path, |object| { + let Some(spot) = unsigned_field(object, "S_fp") else { + return; + }; + rows += 1; + match heston_price( + spot, + unsigned_field(object, "K_fp").unwrap(), + unsigned_field(object, "r_fp").unwrap(), + unsigned_field(object, "T_fp").unwrap(), + unsigned_field(object, "v0_fp").unwrap(), + unsigned_field(object, "kappa_fp").unwrap(), + unsigned_field(object, "theta_fp").unwrap(), + unsigned_field(object, "xi_fp").unwrap(), + numeric_field(object, "rho_fp").unwrap(), + ) { + Ok(_) => accepted += 1, + Err(error) => *failures.entry(format!("{error:?}")).or_default() += 1, + } + }); + assert_eq!(rows, 100_000, "Heston corpus size"); + println!( + "metric=heston_stochastic_fail_closed vectors={rows} accepted={accepted} failure_kinds={failures:?}" + ); + assert_eq!(accepted, 0); + assert_eq!(failures.get("NoConvergence"), Some(&rows)); +} + +fn measure_nig_fail_closed(i128_path: &str, i64_path: &str) { + let mut accepted = 0usize; + let mut failures: BTreeMap = BTreeMap::new(); + let mut rows = 0usize; + for_each_object(i128_path, |object| { + let Some(spot) = unsigned_field(object, "s") else { + return; + }; + rows += 1; + match nig_call_price( + spot, + unsigned_field(object, "k").unwrap(), + unsigned_field(object, "r").unwrap(), + unsigned_field(object, "t").unwrap(), + unsigned_field(object, "alpha").unwrap(), + numeric_field(object, "beta").unwrap(), + unsigned_field(object, "delta").unwrap(), + ) { + Ok(_) => accepted += 1, + Err(error) => *failures.entry(format!("{error:?}")).or_default() += 1, + } + }); + assert_eq!(rows, 1_000, "NIG i128 corpus size"); + println!( + "metric=nig_call_price_fail_closed vectors={rows} accepted={accepted} failure_kinds={failures:?}" + ); + assert_eq!(accepted, 0); + assert_eq!(failures.get("NoConvergence"), Some(&rows)); + + let mut call_accepted = 0usize; + let mut put_accepted = 0usize; + let mut call_failures: BTreeMap = BTreeMap::new(); + let mut put_failures: BTreeMap = BTreeMap::new(); + let mut rows = 0usize; + for_each_object(i64_path, |object| { + let Some(spot) = numeric_field(object, "s").and_then(|value| i64::try_from(value).ok()) + else { + return; + }; + rows += 1; + let strike = i64::try_from(numeric_field(object, "k").unwrap()).unwrap(); + let rate = i64::try_from(numeric_field(object, "r").unwrap()).unwrap(); + let time = i64::try_from(numeric_field(object, "t").unwrap()).unwrap(); + let alpha = i64::try_from(numeric_field(object, "alpha").unwrap()).unwrap(); + let beta = i64::try_from(numeric_field(object, "beta").unwrap()).unwrap(); + let delta = i64::try_from(numeric_field(object, "delta_param").unwrap()).unwrap(); + match nig_call_64(spot, strike, rate, time, alpha, beta, delta) { + Ok(_) => call_accepted += 1, + Err(error) => *call_failures.entry(format!("{error:?}")).or_default() += 1, + } + match nig_put_64(spot, strike, rate, time, alpha, beta, delta) { + Ok(_) => put_accepted += 1, + Err(error) => *put_failures.entry(format!("{error:?}")).or_default() += 1, + } + }); + assert_eq!(rows, 200, "NIG i64 corpus size"); + println!( + "metric=nig_call_64_fail_closed vectors={rows} accepted={call_accepted} failure_kinds={call_failures:?}" + ); + println!( + "metric=nig_put_64_fail_closed vectors={rows} accepted={put_accepted} failure_kinds={put_failures:?}" + ); + assert_eq!(call_accepted, 0); + assert_eq!(put_accepted, 0); + assert_eq!(call_failures.get("NoConvergence"), Some(&rows)); + assert_eq!(put_failures.get("NoConvergence"), Some(&rows)); +} + +fn main() { + let paths: Vec = env::args().skip(1).collect(); + assert_eq!( + paths.len(), + 6, + "pass BVN, Phi2, SABR, Heston, NIG-i128, and NIG-i64 JSON paths" + ); + measure_bvn(&paths[0]); + measure_phi2(&paths[1]); + measure_sabr(&paths[2]); + measure_heston_fail_closed(&paths[3]); + measure_nig_fail_closed(&paths[4], &paths[5]); +} diff --git a/scripts/measure_exp_affected.rs b/scripts/measure_exp_affected.rs new file mode 100644 index 0000000..a043eff --- /dev/null +++ b/scripts/measure_exp_affected.rs @@ -0,0 +1,467 @@ +//! Dependency-free retained-corpus runner for paths that call `exp_fixed_i`. +//! +//! Build `solmath` with all features, compile this file with `rustc`, then +//! pass the benchmark directory containing the retained JSON vectors. + +use solmath::{ + black_scholes_price, bs_delta, bs_full, bs_gamma, bs_rho, bs_theta, bs_vega, exp_fixed_i, + fp_mul_i, implied_vol, ln_fixed_i, norm_cdf_and_pdf, norm_pdf, pow_fixed, pow_fixed_i, + SolMathError, +}; +use std::{collections::BTreeMap, env, fs, path::Path}; + +#[derive(Default)] +struct Stats { + errors: Vec, + failures: BTreeMap, +} + +impl Stats { + fn accept_u(&mut self, actual: u128, expected: u128) { + self.errors.push(actual.abs_diff(expected)); + } + + fn accept_i(&mut self, actual: i128, expected: i128) { + self.errors.push(actual.abs_diff(expected)); + } + + fn fail(&mut self, error: SolMathError) { + *self.failures.entry(format!("{error:?}")).or_default() += 1; + } + + fn print(mut self, name: &str) { + self.errors.sort_unstable(); + let accepted = self.errors.len(); + let failures: usize = self.failures.values().sum(); + let percentile = |numerator: usize, denominator: usize| -> u128 { + if accepted == 0 { + 0 + } else { + self.errors[(accepted - 1) * numerator / denominator] + } + }; + let exact = self.errors.partition_point(|error| *error == 0); + println!( + "metric={name} vectors={} accepted={accepted} failures={failures} max={} p99={} p95={} median={} exact={} exact_pct={:.6} failure_kinds={:?}", + accepted + failures, + self.errors.last().copied().unwrap_or(0), + percentile(99, 100), + percentile(95, 100), + percentile(1, 2), + exact, + if accepted == 0 { 0.0 } else { exact as f64 * 100.0 / accepted as f64 }, + self.failures, + ); + } +} + +fn numeric_field(object: &str, key: &str) -> Option { + let needle = format!("\"{key}\""); + let key_at = object.find(&needle)?; + let colon = object[key_at + needle.len()..].find(':')? + key_at + needle.len(); + let bytes = object.as_bytes(); + let mut cursor = colon + 1; + while bytes.get(cursor).is_some_and(u8::is_ascii_whitespace) { + cursor += 1; + } + if bytes.get(cursor) == Some(&b'\"') { + cursor += 1; + } + let start = cursor; + if bytes.get(cursor) == Some(&b'-') { + cursor += 1; + } + while bytes.get(cursor).is_some_and(u8::is_ascii_digit) { + cursor += 1; + } + object[start..cursor].parse().ok() +} + +fn unsigned_field(object: &str, key: &str) -> Option { + u128::try_from(numeric_field(object, key)?).ok() +} + +fn for_each_object(path: &Path, mut visit: impl FnMut(&str)) -> usize { + let input = fs::read_to_string(path).unwrap_or_else(|error| panic!("read {path:?}: {error}")); + let mut cursor = input.find("\"vectors\"").unwrap_or(0); + let mut rows = 0; + while let Some(relative_start) = input[cursor..].find('{') { + let start = cursor + relative_start; + let end = start + input[start..].find('}').expect("terminated JSON object") + 1; + let object = &input[start..end]; + if object.contains("\"expected\"") + || object.contains("\"expected_pdf\"") + || object.contains("\"call\"") + || object.contains("\"call_price\"") + { + visit(object); + rows += 1; + } + cursor = end; + } + rows +} + +fn measure_norm_pdf(directory: &Path) { + let mut direct = Stats::default(); + let rows = for_each_object(&directory.join("prod_norm_pdf_vectors.json"), |object| { + let x = numeric_field(object, "x").expect("x"); + let expected = numeric_field(object, "expected").expect("expected"); + match norm_pdf(x) { + Ok(actual) => direct.accept_i(actual, expected), + Err(error) => direct.fail(error), + } + }); + assert_eq!(rows, 100_000); + direct.print("norm_pdf.production"); + + let mut cdf = Stats::default(); + let mut pdf = Stats::default(); + let rows = for_each_object(&directory.join("prod_cdf_pdf_vectors.json"), |object| { + let x = numeric_field(object, "x").expect("x"); + let expected_cdf = numeric_field(object, "expected_cdf").expect("expected_cdf"); + let expected_pdf = numeric_field(object, "expected_pdf").expect("expected_pdf"); + match norm_cdf_and_pdf(x) { + Ok((actual_cdf, actual_pdf)) => { + cdf.accept_i(actual_cdf, expected_cdf); + pdf.accept_i(actual_pdf, expected_pdf); + } + Err(error) => { + cdf.fail(error); + pdf.fail(error); + } + } + }); + assert_eq!(rows, 50_000); + cdf.print("norm_cdf_and_pdf.cdf.production"); + pdf.print("norm_cdf_and_pdf.pdf.production"); +} + +fn measure_pow(directory: &Path) { + for file in ["prod_pow_fixed_vectors.json", "adv_pow_fixed_vectors.json"] { + let mut stats = Stats::default(); + let rows = for_each_object(&directory.join(file), |object| { + let base = unsigned_field(object, "base").expect("base"); + let exponent = unsigned_field(object, "exp").expect("exp"); + let expected = unsigned_field(object, "expected").expect("expected"); + match pow_fixed(base, exponent) { + Ok(actual) => stats.accept_u(actual, expected), + Err(error) => stats.fail(error), + } + }); + stats.print(&format!("pow_fixed.{file}.rows_{rows}")); + } + + let mut signed = Stats::default(); + let rows = for_each_object(&directory.join("prod_pow_fixed_i_vectors.json"), |object| { + let base = numeric_field(object, "base").expect("base"); + let exponent = numeric_field(object, "exp").expect("exp"); + let expected = numeric_field(object, "expected").expect("expected"); + match pow_fixed_i(base, exponent) { + Ok(actual) => signed.accept_i(actual, expected), + Err(error) => signed.fail(error), + } + }); + assert_eq!(rows, 50_000); + signed.print("pow_fixed_i.production"); +} + +#[derive(Clone, Copy)] +struct BsRow { + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, + call: u128, + put: u128, + call_delta: Option, + put_delta: Option, + gamma: Option, + vega: Option, + call_theta: Option, + put_theta: Option, + call_rho: Option, + put_rho: Option, +} + +fn bs_row(object: &str) -> BsRow { + BsRow { + s: unsigned_field(object, "s").expect("s"), + k: unsigned_field(object, "k").expect("k"), + r: unsigned_field(object, "r").expect("r"), + sigma: unsigned_field(object, "sigma").expect("sigma"), + t: unsigned_field(object, "t").expect("t"), + call: unsigned_field(object, "call").expect("call"), + put: unsigned_field(object, "put").expect("put"), + call_delta: numeric_field(object, "call_delta"), + put_delta: numeric_field(object, "put_delta"), + gamma: numeric_field(object, "gamma"), + vega: numeric_field(object, "vega"), + call_theta: numeric_field(object, "call_theta"), + put_theta: numeric_field(object, "put_theta"), + call_rho: numeric_field(object, "call_rho"), + put_rho: numeric_field(object, "put_rho"), + } +} + +fn measure_bs_price(directory: &Path) { + let mut call = Stats::default(); + let mut put = Stats::default(); + let rows = for_each_object( + &directory.join("prod_black_scholes_price_vectors.json"), + |object| { + let row = bs_row(object); + match black_scholes_price(row.s, row.k, row.r, row.sigma, row.t) { + Ok((actual_call, actual_put)) => { + call.accept_u(actual_call, row.call); + put.accept_u(actual_put, row.put); + } + Err(error) => { + call.fail(error); + put.fail(error); + } + } + }, + ); + assert_eq!(rows, 50_000); + call.print("black_scholes_price.call.production"); + put.print("black_scholes_price.put.production"); +} + +fn measure_bs_full_file(directory: &Path, file: &str) { + let mut stats: BTreeMap<&'static str, Stats> = BTreeMap::new(); + let rows = for_each_object(&directory.join(file), |object| { + let row = bs_row(object); + match bs_full(row.s, row.k, row.r, row.sigma, row.t) { + Ok(actual) => { + stats + .entry("full.call") + .or_default() + .accept_u(actual.call, row.call); + stats + .entry("full.put") + .or_default() + .accept_u(actual.put, row.put); + stats + .entry("full.call_delta") + .or_default() + .accept_i(actual.call_delta, row.call_delta.unwrap()); + stats + .entry("full.put_delta") + .or_default() + .accept_i(actual.put_delta, row.put_delta.unwrap()); + stats + .entry("full.gamma") + .or_default() + .accept_i(actual.gamma, row.gamma.unwrap()); + stats + .entry("full.vega") + .or_default() + .accept_i(actual.vega, row.vega.unwrap()); + stats + .entry("full.call_theta") + .or_default() + .accept_i(actual.call_theta, row.call_theta.unwrap()); + stats + .entry("full.put_theta") + .or_default() + .accept_i(actual.put_theta, row.put_theta.unwrap()); + stats + .entry("full.call_rho") + .or_default() + .accept_i(actual.call_rho, row.call_rho.unwrap()); + stats + .entry("full.put_rho") + .or_default() + .accept_i(actual.put_rho, row.put_rho.unwrap()); + } + Err(error) => { + for name in [ + "full.call", + "full.put", + "full.call_delta", + "full.put_delta", + "full.gamma", + "full.vega", + "full.call_theta", + "full.put_theta", + "full.call_rho", + "full.put_rho", + ] { + stats.entry(name).or_default().fail(error); + } + } + } + + let mut pair = + |name_a, name_b, result: Result<(i128, i128), SolMathError>, expected_a, expected_b| { + match result { + Ok((a, b)) => { + stats.entry(name_a).or_default().accept_i(a, expected_a); + stats.entry(name_b).or_default().accept_i(b, expected_b); + } + Err(error) => { + stats.entry(name_a).or_default().fail(error); + stats.entry(name_b).or_default().fail(error); + } + } + }; + pair( + "delta.call", + "delta.put", + bs_delta(row.s, row.k, row.r, row.sigma, row.t), + row.call_delta.unwrap(), + row.put_delta.unwrap(), + ); + pair( + "theta.call", + "theta.put", + bs_theta(row.s, row.k, row.r, row.sigma, row.t), + row.call_theta.unwrap(), + row.put_theta.unwrap(), + ); + pair( + "rho.call", + "rho.put", + bs_rho(row.s, row.k, row.r, row.sigma, row.t), + row.call_rho.unwrap(), + row.put_rho.unwrap(), + ); + for (name, result, expected) in [ + ( + "gamma", + bs_gamma(row.s, row.k, row.r, row.sigma, row.t), + row.gamma.unwrap(), + ), + ( + "vega", + bs_vega(row.s, row.k, row.r, row.sigma, row.t), + row.vega.unwrap(), + ), + ] { + match result { + Ok(actual) => stats.entry(name).or_default().accept_i(actual, expected), + Err(error) => stats.entry(name).or_default().fail(error), + } + } + }); + for (name, metric) in stats { + metric.print(&format!("bs.{name}.{file}.rows_{rows}")); + } +} + +fn measure_iv(directory: &Path) { + for file in [ + "prod_implied_vol_vectors.json", + "adv_implied_vol_vectors.json", + ] { + let mut stats = Stats::default(); + let rows = for_each_object(&directory.join(file), |object| { + let market = unsigned_field(object, "call_price").expect("call_price"); + let s = unsigned_field(object, "s").expect("s"); + let k = unsigned_field(object, "k").expect("k"); + let r = unsigned_field(object, "r").expect("r"); + let t = unsigned_field(object, "t").expect("t"); + let expected = unsigned_field(object, "sigma").expect("sigma"); + match implied_vol(market, s, k, r, t) { + Ok(actual) => stats.accept_u(actual, expected), + Err(error) => stats.fail(error), + } + }); + stats.print(&format!("implied_vol.{file}.rows_{rows}")); + } +} + +fn dump_iv_outcomes(directory: &Path, file: &str) { + let mut index = 0usize; + for_each_object(&directory.join(file), |object| { + let market = unsigned_field(object, "call_price").expect("call_price"); + let s = unsigned_field(object, "s").expect("s"); + let k = unsigned_field(object, "k").expect("k"); + let r = unsigned_field(object, "r").expect("r"); + let t = unsigned_field(object, "t").expect("t"); + let expected = unsigned_field(object, "sigma").expect("sigma"); + let r_t = fp_mul_i(r as i128, t as i128).expect("r*t"); + let discount = exp_fixed_i(-r_t).expect("discount"); + let k_disc = fp_mul_i(k as i128, discount).expect("discounted strike"); + let lower = (s as i128).saturating_sub(k_disc).max(0) as u128; + let lower_margin = market as i128 - lower as i128; + let expected_call = black_scholes_price(s, k, r, expected, t) + .expect("BS at expected sigma") + .0; + let expected_price_error = expected_call.abs_diff(market); + match implied_vol(market, s, k, r, t) { + Ok(actual) => { + let result_call = black_scholes_price(s, k, r, actual, t) + .expect("BS at recovered sigma") + .0; + let result_price_error = result_call.abs_diff(market); + println!( + "iv_outcome\t{index}\t{s}\t{k}\t{r}\t{t}\t{expected}\t{market}\t{discount}\t{lower}\t{lower_margin}\tOk\t{actual}\t{expected_call}\t{expected_price_error}\t{result_call}\t{result_price_error}" + ); + } + Err(error) => println!( + "iv_outcome\t{index}\t{s}\t{k}\t{r}\t{t}\t{expected}\t{market}\t{discount}\t{lower}\t{lower_margin}\tErr\t{error:?}\t{expected_call}\t{expected_price_error}\t-\t-" + ), + } + index += 1; + }); +} + +fn dump_pow_bs_outcomes(directory: &Path) { + let mut index = 0usize; + for_each_object(&directory.join("prod_pow_fixed_vectors.json"), |object| { + let base = unsigned_field(object, "base").expect("base"); + let exponent = unsigned_field(object, "exp").expect("exp"); + let expected = unsigned_field(object, "expected").expect("expected"); + let actual = pow_fixed(base, exponent).expect("pow_fixed"); + let ln_base = ln_fixed_i(base).expect("ln(base)"); + let product = i128::try_from(exponent) + .ok() + .and_then(|value| fp_mul_i(value, ln_base).ok()); + let exp_value = product.and_then(|value| exp_fixed_i(value).ok()); + println!( + "pow_outcome\t{index}\t{base}\t{exponent}\t{expected}\t{actual}\t{}\t{}", + product.map_or_else(|| "-".into(), |value| value.to_string()), + exp_value.map_or_else(|| "-".into(), |value| value.to_string()), + ); + index += 1; + }); + + index = 0; + for_each_object( + &directory.join("prod_black_scholes_price_vectors.json"), + |object| { + let row = bs_row(object); + let (call, put) = black_scholes_price(row.s, row.k, row.r, row.sigma, row.t) + .expect("black_scholes_price"); + let r_t = fp_mul_i(row.r as i128, row.t as i128).expect("r*t"); + let discount = exp_fixed_i(-r_t).expect("discount"); + println!( + "bs_outcome\t{index}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{call}\t{put}\t{r_t}\t{discount}", + row.s, row.k, row.r, row.sigma, row.t, row.call, row.put, + ); + index += 1; + }, + ); +} + +fn main() { + let directory = env::args().nth(1).expect("benchmark directory"); + let directory = Path::new(&directory); + if env::args().nth(2).as_deref() == Some("dump-iv") { + dump_iv_outcomes(directory, "prod_implied_vol_vectors.json"); + return; + } + if env::args().nth(2).as_deref() == Some("dump-pow-bs") { + dump_pow_bs_outcomes(directory); + return; + } + measure_norm_pdf(directory); + measure_pow(directory); + measure_bs_price(directory); + measure_bs_full_file(directory, "prod_bs_full_vectors.json"); + measure_bs_full_file(directory, "adv_bs_full_vectors.json"); + measure_iv(directory); +} diff --git a/scripts/measure_expm1_vectors.rs b/scripts/measure_expm1_vectors.rs new file mode 100644 index 0000000..f1c19c6 --- /dev/null +++ b/scripts/measure_expm1_vectors.rs @@ -0,0 +1,147 @@ +//! Dependency-free accuracy runner for retained `expm1_fixed` corpora. +//! +//! Build and run from the repository root: +//! `cargo build --release --all-features` +//! `rustc --edition=2021 scripts/measure_expm1_vectors.rs \ +//! --extern solmath=target/release/libsolmath.rlib \ +//! -L dependency=target/release/deps -o target/measure-expm1` +//! `target/measure-expm1 benchmark/prod_expm1_vectors.json \ +//! benchmark/adv_expm1_vectors.json` + +use solmath::expm1_fixed; +use std::{collections::BTreeMap, env, fs}; + +fn quoted_i128_after(input: &str, key: &str, start: usize) -> Option<(i128, usize)> { + let key_at = input[start..].find(key)? + start; + let colon = input[key_at + key.len()..].find(':')? + key_at + key.len(); + let mut cursor = colon + 1; + while input.as_bytes().get(cursor)?.is_ascii_whitespace() { + cursor += 1; + } + let quoted = input.as_bytes().get(cursor) == Some(&b'"'); + if quoted { + cursor += 1; + } + let number_start = cursor; + if input.as_bytes().get(cursor) == Some(&b'-') { + cursor += 1; + } + while input.as_bytes().get(cursor).is_some_and(u8::is_ascii_digit) { + cursor += 1; + } + if cursor == number_start || (cursor == number_start + 1 && &input[number_start..cursor] == "-") { + return None; + } + if quoted && input.as_bytes().get(cursor) != Some(&b'"') { + return None; + } + Some((input[number_start..cursor].parse().ok()?, cursor + usize::from(quoted))) +} + +fn quoted_string_after(input: &str, key: &str, start: usize) -> Option<(String, usize)> { + let key_at = input[start..].find(key)? + start; + let colon = input[key_at + key.len()..].find(':')? + key_at + key.len(); + let quote = input[colon + 1..].find('"')? + colon + 1; + let end = input[quote + 1..].find('"')? + quote + 1; + Some((input[quote + 1..end].to_owned(), end + 1)) +} + +fn load(path: &str) -> Vec<(i128, i128, String)> { + let input = fs::read_to_string(path).unwrap_or_else(|error| panic!("read {path}: {error}")); + let mut rows = Vec::new(); + let mut cursor = input.find("\"vectors\"").expect("vectors array"); + while let Some((x, after_x)) = quoted_i128_after(&input, "\"x\"", cursor) { + let Some((expected, after_expected)) = quoted_i128_after(&input, "\"expected\"", after_x) else { + break; + }; + let (category, after_category) = quoted_string_after(&input, "\"category\"", after_expected) + .unwrap_or_else(|| ("uncategorized".to_owned(), after_expected)); + rows.push((x, expected, category)); + cursor = after_category; + } + rows +} + +fn percentile(sorted: &[u128], numerator: usize, denominator: usize) -> u128 { + sorted[(sorted.len() - 1) * numerator / denominator] +} + +fn print_stats(file: &str, category: &str, errors: &mut [u128], failures: usize) { + errors.sort_unstable(); + let exact = errors.partition_point(|error| *error == 0); + println!( + "{{\"file\":\"{}\",\"category\":\"{}\",\"vectors\":{},\"accepted\":{},\"failures\":{},\"max\":{},\"p99\":{},\"p95\":{},\"median\":{},\"exact\":{},\"exact_pct\":{:.6}}}", + file, + category, + errors.len() + failures, + errors.len(), + failures, + errors.last().copied().unwrap_or(0), + percentile(errors, 99, 100), + percentile(errors, 95, 100), + percentile(errors, 1, 2), + exact, + exact as f64 * 100.0 / errors.len() as f64, + ); +} + +fn measure(path: &str) { + let rows = load(path); + let mut errors = Vec::with_capacity(rows.len()); + let mut failures = 0usize; + let mut categories: BTreeMap> = BTreeMap::new(); + let mut ordinary_errors = Vec::new(); + let mut max_error = 0u128; + let mut max_relative_large_output_ppt = 0.0f64; + let mut worst_rows = Vec::new(); + for (x, expected, category) in rows { + match expm1_fixed(x) { + Ok(actual) => { + let error = actual.abs_diff(expected); + if error > max_error { + max_error = error; + worst_rows.clear(); + } + if error == max_error && worst_rows.len() < 20 { + worst_rows.push((x, expected, actual, category.clone())); + } + if x.unsigned_abs() <= 2_000_000_000_000 { + ordinary_errors.push(error); + } + if expected.unsigned_abs() >= 1_000_000_000_000 { + max_relative_large_output_ppt = max_relative_large_output_ppt.max( + error as f64 / expected.unsigned_abs() as f64 * 1_000_000_000_000.0, + ); + } + errors.push(error); + categories.entry(category).or_default().push(error); + } + Err(_) => failures += 1, + } + } + print_stats(path, "all", &mut errors, failures); + if !ordinary_errors.is_empty() { + print_stats(path, "ordinary_abs_le_2", &mut ordinary_errors, 0); + } + println!( + "{{\"file\":\"{}\",\"metric\":\"max_relative_parts_per_trillion_for_outputs_ge_1\",\"value\":{:.12}}}", + path, max_relative_large_output_ppt + ); + for (category, mut category_errors) in categories { + print_stats(path, &category, &mut category_errors, 0); + } + for (x, expected, actual, category) in worst_rows { + println!( + "{{\"file\":\"{}\",\"worst\":true,\"x\":\"{}\",\"category\":\"{}\",\"expected\":\"{}\",\"actual\":\"{}\",\"error\":{}}}", + path, x, category, expected, actual, max_error + ); + } +} + +fn main() { + let paths: Vec = env::args().skip(1).collect(); + assert!(!paths.is_empty(), "pass one or more vector JSON files"); + for path in paths { + measure(&path); + } +} diff --git a/scripts/measure_heston_xi0.rs b/scripts/measure_heston_xi0.rs new file mode 100644 index 0000000..26a3e67 --- /dev/null +++ b/scripts/measure_heston_xi0.rs @@ -0,0 +1,73 @@ +//! Emit the deterministic 200,704-case Heston grid for independent analysis. + +use solmath::{fp_mul, heston_price, SCALE}; + +fn main() { + let spots = [1u128, 100]; + let strike_multipliers = [ + 250_000_000_000u128, + 500_000_000_000, + 800_000_000_000, + SCALE, + 1_200_000_000_000, + 2 * SCALE, + 4 * SCALE, + ]; + let rates = [0u128, 10_000_000_000, 50_000_000_000, 200_000_000_000]; + let times = [ + 1_000_000_000u128, + 10_000_000_000, + 100_000_000_000, + 500_000_000_000, + SCALE, + 2 * SCALE, + 10 * SCALE, + 100 * SCALE, + ]; + let variances = [ + 0u128, + 1_000_000, + 100_000_000, + 10_000_000_000, + 40_000_000_000, + 250_000_000_000, + SCALE, + 4 * SCALE, + ]; + let kappas = [ + 0u128, + 1_000_000, + 1_000_000_000, + 10_000_000_000, + 500_000_000_000, + 2 * SCALE, + 20 * SCALE, + ]; + + for &spot_units in &spots { + for &strike_multiplier in &strike_multipliers { + for &rate in &rates { + for &time in × { + for &v0 in &variances { + for &theta in &variances { + for &kappa in &kappas { + let spot = spot_units * SCALE; + let strike = fp_mul(spot, strike_multiplier).unwrap(); + match heston_price( + spot, strike, rate, time, v0, kappa, theta, 0, 0, + ) { + Ok((call, put)) => println!( + "{spot},{strike},{rate},{time},{v0},{kappa},{theta},{call},{put}" + ), + Err(error) => println!( + "{spot},{strike},{rate},{time},{v0},{kappa},{theta},E{error:?},E{error:?}" + ), + } + } + } + } + } + } + } + } +} diff --git a/scripts/measure_ln_1p_vectors.rs b/scripts/measure_ln_1p_vectors.rs new file mode 100644 index 0000000..14860ef --- /dev/null +++ b/scripts/measure_ln_1p_vectors.rs @@ -0,0 +1,136 @@ +//! Dependency-free accuracy runner for the generated ln_1p vector corpora. +//! +//! Build and run from the repository root: +//! `cargo build --release --all-features` +//! `rustc --edition=2021 scripts/measure_ln_1p_vectors.rs \ +//! --extern solmath=target/release/libsolmath.rlib \ +//! -L dependency=target/release/deps -o target/measure-ln-1p` +//! `target/measure-ln-1p benchmark/prod_ln_1p_vectors.json \ +//! benchmark/adv_ln_1p_vectors.json` + +use solmath::ln_1p_fixed; +use std::{collections::BTreeMap, env, fs}; + +fn quoted_i128_after(input: &str, key: &str, start: usize) -> Option<(i128, usize)> { + let key_at = input[start..].find(key)? + start; + let colon = input[key_at + key.len()..].find(':')? + key_at + key.len(); + let mut cursor = colon + 1; + while input.as_bytes().get(cursor)?.is_ascii_whitespace() { + cursor += 1; + } + let quoted = input.as_bytes().get(cursor) == Some(&b'"'); + if quoted { + cursor += 1; + } + let number_start = cursor; + if input.as_bytes().get(cursor) == Some(&b'-') { + cursor += 1; + } + while input.as_bytes().get(cursor).is_some_and(u8::is_ascii_digit) { + cursor += 1; + } + if cursor == number_start || (cursor == number_start + 1 && &input[number_start..cursor] == "-") { + return None; + } + if quoted && input.as_bytes().get(cursor) != Some(&b'"') { + return None; + } + Some((input[number_start..cursor].parse().ok()?, cursor + usize::from(quoted))) +} + +fn quoted_string_after(input: &str, key: &str, start: usize) -> Option<(String, usize)> { + let key_at = input[start..].find(key)? + start; + let colon = input[key_at + key.len()..].find(':')? + key_at + key.len(); + let quote = input[colon + 1..].find('"')? + colon + 1; + let end = input[quote + 1..].find('"')? + quote + 1; + Some((input[quote + 1..end].to_owned(), end + 1)) +} + +fn load(path: &str) -> Vec<(i128, i128, String)> { + let input = fs::read_to_string(path).unwrap_or_else(|error| panic!("read {path}: {error}")); + let mut rows = Vec::new(); + let mut cursor = input.find("\"vectors\"").expect("vectors array"); + while let Some((x, after_x)) = quoted_i128_after(&input, "\"x\"", cursor) { + let Some((expected, after_expected)) = + quoted_i128_after(&input, "\"expected\"", after_x) + else { + break; + }; + let (category, after_category) = quoted_string_after( + &input, + "\"category\"", + after_expected, + ) + .unwrap_or_else(|| ("uncategorized".to_owned(), after_expected)); + rows.push((x, expected, category)); + cursor = after_category; + } + rows +} + +fn percentile(sorted: &[u128], numerator: usize, denominator: usize) -> u128 { + sorted[(sorted.len() - 1) * numerator / denominator] +} + +fn print_stats(file: &str, category: &str, errors: &mut [u128], failures: usize) { + errors.sort_unstable(); + let exact = errors.partition_point(|error| *error == 0); + println!( + "{{\"file\":\"{}\",\"category\":\"{}\",\"vectors\":{},\"accepted\":{},\"failures\":{},\"max\":{},\"p99\":{},\"p95\":{},\"median\":{},\"exact\":{},\"exact_pct\":{:.6}}}", + file, + category, + errors.len() + failures, + errors.len(), + failures, + errors.last().copied().unwrap_or(0), + percentile(errors, 99, 100), + percentile(errors, 95, 100), + percentile(errors, 1, 2), + exact, + exact as f64 * 100.0 / errors.len() as f64, + ); +} + +fn measure(path: &str) { + let rows = load(path); + let mut errors = Vec::with_capacity(rows.len()); + let mut failures = 0usize; + let mut categories: BTreeMap> = BTreeMap::new(); + let mut max_error = 0u128; + let mut worst_rows = Vec::new(); + for (x, expected, category) in rows { + match ln_1p_fixed(x) { + Ok(actual) => { + let error = actual.abs_diff(expected); + if error > max_error { + max_error = error; + worst_rows.clear(); + } + if error == max_error && worst_rows.len() < 20 { + worst_rows.push((x, expected, actual, category.clone())); + } + errors.push(error); + categories.entry(category).or_default().push(error); + } + Err(_) => failures += 1, + } + } + print_stats(path, "all", &mut errors, failures); + for (category, mut category_errors) in categories { + print_stats(path, &category, &mut category_errors, 0); + } + for (x, expected, actual, category) in worst_rows { + println!( + "{{\"file\":\"{}\",\"worst\":true,\"x\":\"{}\",\"category\":\"{}\",\"expected\":\"{}\",\"actual\":\"{}\",\"error\":{}}}", + path, x, category, expected, actual, max_error + ); + } +} + +fn main() { + let paths: Vec = env::args().skip(1).collect(); + assert!(!paths.is_empty(), "pass one or more vector JSON files"); + for path in paths { + measure(&path); + } +} diff --git a/scripts/measure_sbf_footprint.sh b/scripts/measure_sbf_footprint.sh new file mode 100755 index 0000000..de76997 --- /dev/null +++ b/scripts/measure_sbf_footprint.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +manifest="$repo_root/benchmark/sbf-footprint/Cargo.toml" +artifact="$repo_root/benchmark/sbf-footprint/target/deploy/solmath_sbf_footprint.so" +log_dir="$(mktemp -d)" +trap 'rm -rf "$log_dir"' EXIT + +cargo metadata --manifest-path "$manifest" --locked --no-deps --format-version 1 \ + >"$log_dir/metadata.json" + +build_size() { + local label="$1" + local feature="$2" + local -a command=( + cargo build-sbf + --manifest-path "$manifest" + --no-default-features + ) + if [[ -n "$feature" ]]; then + command+=(--features "$feature") + fi + if ! "${command[@]}" >"$log_dir/$label.log" 2>&1; then + cat "$log_dir/$label.log" >&2 + return 1 + fi + wc -c <"$artifact" | tr -d ' ' +} + +baseline="$(build_size baseline "")" +exp="$(build_size exp exp)" +expm1="$(build_size expm1 expm1)" +ln1p="$(build_size ln1p ln1p)" +both="$(build_size both both)" +legacy_expm1="$(build_size legacy-expm1 legacy-expm1)" +kbi="$(build_size kbi kbi)" +nig="$(build_size nig nig)" + +exp_delta=$((exp - baseline)) +expm1_delta=$((expm1 - baseline)) +ln1p_delta=$((ln1p - baseline)) +both_delta=$((both - baseline)) +legacy_expm1_delta=$((legacy_expm1 - baseline)) +kbi_delta=$((kbi - baseline)) +nig_delta=$((nig - baseline)) + +printf '%-16s %12s %12s\n' "variant" "SBF bytes" "delta" +printf '%-16s %12d %12d\n' "baseline" "$baseline" 0 +printf '%-16s %12d %12d\n' "exp" "$exp" "$exp_delta" +printf '%-16s %12d %12d\n' "expm1" "$expm1" "$expm1_delta" +printf '%-16s %12d %12d\n' "ln1p" "$ln1p" "$ln1p_delta" +printf '%-16s %12d %12d\n' "both" "$both" "$both_delta" +printf '%-16s %12d %12d\n' "legacy-expm1" "$legacy_expm1" "$legacy_expm1_delta" +printf '%-16s %12d %12d\n' "kbi" "$kbi" "$kbi_delta" +printf '%-16s %12d %12d\n' "nig" "$nig" "$nig_delta" + +status=0 +check_delta() { + local name="$1" + local actual="$2" + local budget="$3" + if ((actual > budget)); then + echo "$name linked delta $actual exceeds budget $budget" >&2 + status=1 + fi +} + +check_delta expm1 "$expm1_delta" $((22 * 1024)) +check_delta ln1p "$ln1p_delta" $((34 * 1024)) +check_delta both "$both_delta" $((50 * 1024)) +check_delta exp "$exp_delta" $((10 * 1024)) +check_delta kbi "$kbi_delta" $((140 * 1024)) +check_delta nig "$nig_delta" $((125 * 1024)) +if ((expm1 >= legacy_expm1)); then + echo "hybrid expm1 ($expm1 bytes) is not smaller than legacy expm1 ($legacy_expm1 bytes)" >&2 + status=1 +fi +exit "$status" diff --git a/scripts/measure_unary_vectors.rs b/scripts/measure_unary_vectors.rs new file mode 100644 index 0000000..f23594a --- /dev/null +++ b/scripts/measure_unary_vectors.rs @@ -0,0 +1,141 @@ +//! Dependency-free accuracy runner for unary production/adversarial corpora. +//! +//! Build the release library, compile this file with `rustc`, then run: +//! `measure-unary exp FILE...`, `measure-unary ln FILE...`, or +//! `measure-unary norm_cdf FILE...`. + +use solmath::{exp_fixed_i, ln_fixed_i, norm_cdf_poly}; +use std::{collections::BTreeMap, env, fs}; + +fn quoted_after(input: &str, key: &str, start: usize) -> Option<(String, usize)> { + let key_at = input[start..].find(key)? + start; + let colon = input[key_at + key.len()..].find(':')? + key_at + key.len(); + let quote = input[colon + 1..].find('"')? + colon + 1; + let end = input[quote + 1..].find('"')? + quote + 1; + Some((input[quote + 1..end].to_owned(), end + 1)) +} + +fn load(path: &str) -> Vec<(String, i128, String)> { + let input = fs::read_to_string(path).unwrap_or_else(|error| panic!("read {path}: {error}")); + let mut rows = Vec::new(); + let mut cursor = input.find("\"vectors\"").expect("vectors array"); + let categorized = input[cursor..].contains("\"category\""); + while let Some((x, after_x)) = quoted_after(&input, "\"x\"", cursor) { + let Some((expected, after_expected)) = quoted_after(&input, "\"expected\"", after_x) + else { + break; + }; + let (category, after_category) = if categorized { + quoted_after(&input, "\"category\"", after_expected) + .unwrap_or_else(|| ("uncategorized".to_owned(), after_expected)) + } else { + ("uncategorized".to_owned(), after_expected) + }; + rows.push((x, expected.parse().expect("i128 expected"), category)); + cursor = after_category; + } + rows +} + +fn percentile(sorted: &[u128], numerator: usize, denominator: usize) -> u128 { + sorted[(sorted.len() - 1) * numerator / denominator] +} + +fn percentile_f64(sorted: &[f64], numerator: usize, denominator: usize) -> f64 { + sorted[(sorted.len() - 1) * numerator / denominator] +} + +fn stats( + file: &str, + category: &str, + errors: &mut [u128], + relative_ppb: &mut [f64], + failures: usize, +) { + errors.sort_unstable(); + relative_ppb.sort_unstable_by(f64::total_cmp); + let exact = errors.partition_point(|error| *error == 0); + println!( + "{{\"file\":\"{}\",\"category\":\"{}\",\"vectors\":{},\"accepted\":{},\"failures\":{},\"max\":{},\"p99\":{},\"p95\":{},\"median\":{},\"exact_pct\":{:.6},\"max_relative_ppb\":{:.12},\"p99_relative_ppb\":{:.12},\"p95_relative_ppb\":{:.12},\"median_relative_ppb\":{:.12}}}", + file, + category, + errors.len() + failures, + errors.len(), + failures, + errors.last().copied().unwrap_or(0), + percentile(errors, 99, 100), + percentile(errors, 95, 100), + percentile(errors, 1, 2), + exact as f64 * 100.0 / errors.len() as f64, + relative_ppb.last().copied().unwrap_or(0.0), + percentile_f64(relative_ppb, 99, 100), + percentile_f64(relative_ppb, 95, 100), + percentile_f64(relative_ppb, 1, 2), + ); +} + +fn evaluate(kind: &str, x: &str) -> Result { + match kind { + "ln" => ln_fixed_i(x.parse().expect("u128 ln input")), + "exp" => exp_fixed_i(x.parse().expect("i128 exp input")), + "norm_cdf" => norm_cdf_poly(x.parse().expect("i128 norm_cdf input")), + _ => panic!("unknown function {kind}; expected exp, ln, or norm_cdf"), + } +} + +fn measure(kind: &str, path: &str) { + let rows = load(path); + let mut errors = Vec::with_capacity(rows.len()); + let mut relative_ppb = Vec::with_capacity(rows.len()); + let mut categories: BTreeMap, Vec)> = BTreeMap::new(); + let mut failures = 0usize; + let mut worst = Vec::new(); + let mut max_error = 0u128; + for (x, expected, category) in rows { + match evaluate(kind, &x) { + Ok(actual) => { + let error = actual.abs_diff(expected); + let relative = error as f64 / expected.unsigned_abs().max(1) as f64 * 1e9; + if error > max_error { + max_error = error; + worst.clear(); + } + if error == max_error && worst.len() < 20 { + worst.push((x, expected, actual, category.clone())); + } + errors.push(error); + relative_ppb.push(relative); + let category_values = categories.entry(category).or_default(); + category_values.0.push(error); + category_values.1.push(relative); + } + Err(_) => failures += 1, + } + } + stats(path, "all", &mut errors, &mut relative_ppb, failures); + for (category, (mut category_errors, mut category_relative_ppb)) in categories { + stats( + path, + &category, + &mut category_errors, + &mut category_relative_ppb, + 0, + ); + } + for (x, expected, actual, category) in worst { + println!( + "{{\"file\":\"{}\",\"worst\":true,\"x\":\"{}\",\"category\":\"{}\",\"expected\":\"{}\",\"actual\":\"{}\",\"error\":{}}}", + path, x, category, expected, actual, max_error + ); + } +} + +fn main() { + let mut args = env::args().skip(1); + let kind = args.next().expect("pass exp, ln, or norm_cdf"); + let paths: Vec = args.collect(); + assert!(!paths.is_empty(), "pass one or more vector files"); + for path in paths { + measure(&kind, &path); + } +} diff --git a/scripts/nig_reference.py b/scripts/nig_reference.py new file mode 100644 index 0000000..085b522 --- /dev/null +++ b/scripts/nig_reference.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +"""Independent high-precision NIG oracle audit. + +Two non-shared representations are evaluated: + +1. direct Bessel-density integration of the out-of-the-money payoff with a + 64-point arbitrary-precision Gauss rule; and +2. Lewis Fourier inversion of the martingale-corrected characteristic + function. + +The script also invokes the compiled fixed-point batch harness and records all +three values. It is intentionally a smaller, expensive audit complement to the +100k/10k SciPy campaign in `validate_nig_runtime.py`. +""" + +from __future__ import annotations + +import argparse +import json +import math +import subprocess +from pathlib import Path + +import mpmath as mp + + +ROOT = Path(__file__).resolve().parents[1] +BINARY = ROOT / "target/release/examples/nig_batch" +SCALE = 10**12 + +# s, k, r, q, t, alpha, beta, delta/year +CASES = [ + (100, 100, 0.05, 0.02, 1.0, 10, -2, 0.2), + (80, 100, 0.03, 0.01, 0.5, 8, -3, 0.4), + (130, 100, -0.01, 0.04, 2.0, 12, 2, 0.3), + (100, 115, -0.04, 0.08, 1.5, 2.5, -0.8, 0.25), + (100, 85, 0.12, -0.03, 0.75, 4, 1, 0.5), + (250, 250, 0.0, 0.0, 0.25, 100, -20, 1.0), +] + + +def m(value: float | int) -> mp.mpf: + return mp.mpf(str(value)) + + +def setup(case: tuple[float, ...]) -> dict[str, mp.mpf | bool]: + spot, strike, rate, dividend, time, alpha, beta, delta_py = map(m, case) + elapsed = delta_py * time + gamma = mp.sqrt(alpha * alpha - beta * beta) + gamma_one = mp.sqrt(alpha * alpha - (beta + 1) ** 2) + correction = elapsed * (gamma_one - gamma) + discounted_spot = spot * mp.exp(-dividend * time) + discounted_strike = strike * mp.exp(-rate * time) + kappa = mp.log(spot / strike) + (rate - dividend) * time + correction + return { + "spot": spot, + "strike": strike, + "rate": rate, + "dividend": dividend, + "time": time, + "alpha": alpha, + "beta": beta, + "elapsed": elapsed, + "gamma": gamma, + "gamma_one": gamma_one, + "correction": correction, + "discounted_spot": discounted_spot, + "discounted_strike": discounted_strike, + "threshold": -kappa, + "call_is_otm": discounted_spot <= discounted_strike, + } + + +def direct_density_price(data: dict, nodes: mp.matrix, weights: mp.matrix) -> tuple[mp.mpf, mp.mpf]: + alpha = data["alpha"] + beta = data["beta"] + elapsed = data["elapsed"] + gamma = data["gamma"] + threshold = data["threshold"] + call_is_otm = data["call_is_otm"] + base_scale = mp.sqrt(elapsed * alpha**2 / gamma**3) + if call_is_otm: + tilted_scale = mp.sqrt(elapsed * alpha**2 / data["gamma_one"] ** 3) + base_scale = max(base_scale, tilted_scale) + scale = 4 * base_scale + + integral = mp.mpf(0) + for node, weight in zip(nodes, weights): + t = (node + 1) / 2 + y = scale * t / (1 - t) + x = threshold + y if call_is_otm else threshold - y + omega = mp.sqrt(elapsed**2 + x**2) + density = ( + alpha + * elapsed + / (mp.pi * omega) + * mp.besselk(1, alpha * omega) + * mp.exp(elapsed * gamma + beta * x) + ) + payoff = mp.expm1(y) if call_is_otm else -mp.expm1(-y) + jacobian = scale / (1 - t) ** 2 / 2 + integral += weight * payoff * density * jacobian + + otm = data["discounted_strike"] * integral + if call_is_otm: + return ( + otm, + otm + data["discounted_strike"] - data["discounted_spot"], + ) + return ( + otm + data["discounted_spot"] - data["discounted_strike"], + otm, + ) + + +def lewis_price(data: dict) -> tuple[mp.mpf, mp.mpf]: + alpha = data["alpha"] + beta = data["beta"] + elapsed = data["elapsed"] + gamma = data["gamma"] + correction = data["correction"] + log_discounted_moneyness = mp.log( + data["discounted_spot"] / data["discounted_strike"] + ) + + def characteristic(z: mp.mpc) -> mp.mpc: + return mp.exp( + 1j * z * correction + + elapsed * (gamma - mp.sqrt(alpha**2 - (beta + 1j * z) ** 2)) + ) + + def integrand(u: mp.mpf) -> mp.mpf: + return mp.re( + mp.exp(1j * u * log_discounted_moneyness) + * characteristic(u - mp.mpf("0.5") * 1j) + ) / (u * u + mp.mpf("0.25")) + + cutoff = 50 / elapsed + points = [mp.mpf(0), mp.mpf(1)] + while points[-1] < cutoff: + points.append(min(cutoff, points[-1] * 2)) + integral = mp.quad(integrand, points) + call = data["discounted_spot"] - mp.sqrt( + data["discounted_spot"] * data["discounted_strike"] + ) / mp.pi * integral + put = call + data["discounted_strike"] - data["discounted_spot"] + return call, put + + +def raw(value: float) -> int: + return round(value * SCALE) + + +def runtime_prices() -> list[tuple[int, int, int, int]]: + lines = [] + for spot, strike, rate, dividend, time, alpha, beta, delta in CASES: + # The audit asks the kernel to return even when its conservative local + # allowance is wider than the production request; actual error is then + # measured against both independent oracles below. + requested = max(1, raw(max(spot, strike) * 1e-2)) + lines.append( + " ".join( + str(value) + for value in ( + raw(spot), + raw(strike), + raw(rate), + raw(dividend), + raw(time), + raw(alpha), + raw(beta), + raw(delta), + requested, + ) + ) + ) + process = subprocess.run( + [str(BINARY)], + input="\n".join(lines) + "\n", + text=True, + capture_output=True, + check=True, + ) + results = [] + for line in process.stdout.splitlines(): + fields = line.split() + if not fields or fields[0] != "OK": + raise RuntimeError(f"fixed-point audit quote rejected: {line}") + results.append(tuple(map(int, fields[1:]))) + return results + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--dps", type=int, default=50) + parser.add_argument( + "--output", + type=Path, + default=ROOT / "benchmark/nig_independent_oracle_report.json", + ) + parser.add_argument("--skip-build", action="store_true") + args = parser.parse_args() + mp.mp.dps = args.dps + + if not args.skip_build: + subprocess.run( + [ + "cargo", + "build", + "--release", + "--no-default-features", + "--features", + "nig", + "--example", + "nig_batch", + ], + cwd=ROOT, + check=True, + ) + + nodes48, weights48 = mp.gauss_quadrature(48, "legendre") + nodes64, weights64 = mp.gauss_quadrature(64, "legendre") + fixed = runtime_prices() + rows = [] + maxima = { + "density_48_vs_64": 0.0, + "density_vs_fourier": 0.0, + "fixed_vs_density": 0.0, + "fixed_vs_fourier": 0.0, + } + + for index, (case, fixed_quote) in enumerate(zip(CASES, fixed)): + data = setup(case) + density48 = direct_density_price(data, nodes48, weights48) + density64 = direct_density_price(data, nodes64, weights64) + fourier = lewis_price(data) + fixed_call, fixed_put, allowance, tier = fixed_quote + fixed_values = (mp.mpf(fixed_call) / SCALE, mp.mpf(fixed_put) / SCALE) + + density_convergence = max(abs(a - b) for a, b in zip(density48, density64)) + oracle_difference = max(abs(a - b) for a, b in zip(density64, fourier)) + fixed_density = max(abs(a - b) for a, b in zip(fixed_values, density64)) + fixed_fourier = max(abs(a - b) for a, b in zip(fixed_values, fourier)) + maxima["density_48_vs_64"] = max(maxima["density_48_vs_64"], float(density_convergence)) + maxima["density_vs_fourier"] = max(maxima["density_vs_fourier"], float(oracle_difference)) + maxima["fixed_vs_density"] = max(maxima["fixed_vs_density"], float(fixed_density)) + maxima["fixed_vs_fourier"] = max(maxima["fixed_vs_fourier"], float(fixed_fourier)) + rows.append( + { + "index": index, + "input": dict(zip(("spot", "strike", "rate", "dividend", "time", "alpha", "beta", "delta_per_year"), case)), + "density_call": mp.nstr(density64[0], args.dps), + "density_put": mp.nstr(density64[1], args.dps), + "fourier_call": mp.nstr(fourier[0], args.dps), + "fourier_put": mp.nstr(fourier[1], args.dps), + "fixed_call": fixed_call / SCALE, + "fixed_put": fixed_put / SCALE, + "returned_max_abs_error": allowance / SCALE, + "tier": tier, + "density_48_vs_64": float(density_convergence), + "density_vs_fourier": float(oracle_difference), + "fixed_vs_density": float(fixed_density), + } + ) + + report = { + "schema": 1, + "mpmath_dps": args.dps, + "methods": [ + "direct NIG Bessel-density OTM integration, arbitrary-precision Gauss-Legendre 64", + "Lewis characteristic-function inversion", + "SolMath fixed-point 15/7 runtime", + ], + "cases": len(rows), + "maxima": maxima, + "rows": rows, + } + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(report, indent=2) + "\n") + print(json.dumps(report, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/requirements-certificates.txt b/scripts/requirements-certificates.txt new file mode 100644 index 0000000..03da3e6 --- /dev/null +++ b/scripts/requirements-certificates.txt @@ -0,0 +1,10 @@ +# Certificate environment: CPython 3.12, Linux x86_64 CI or macOS arm64, +# binary wheels only. Each accepted wheel is hash-locked. +numpy==2.4.2 \ + --hash=sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f \ + --hash=sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e +python-flint==0.8.0 \ + --hash=sha256:af60dbed2b0e3bedef2875ff3a2b32afec12f7152595d65fcd674713ac09a208 \ + --hash=sha256:884a75da741e4ebbfdf5c638629d9e8a34f5bbbbb315b815b08a7d664f7720b2 +mpmath==1.4.1 \ + --hash=sha256:dc4f0ea2304480d4a9a48a94c1020571558ade522b44a6912efac63a586e140f diff --git a/scripts/requirements.txt b/scripts/requirements.txt index a327367..2bd5b3a 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -1,4 +1,5 @@ -mpmath>=1.3.0 -scipy>=1.10.0 -numpy>=1.24.0 -QuantLib-Python>=1.31 +mpmath==1.4.1 +scipy==1.17.0 +numpy==2.4.2 +QuantLib==1.41 +python-flint==0.8.0 diff --git a/scripts/validate_american_kbi_runtime.py b/scripts/validate_american_kbi_runtime.py new file mode 100644 index 0000000..f0602f1 --- /dev/null +++ b/scripts/validate_american_kbi_runtime.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Validate compiled fixed-point Kim Boundary Integration against QuantLib QdFp.""" + +from __future__ import annotations + +import argparse +import json +import math +import pathlib +import subprocess + +import numpy as np +import QuantLib as ql + +from american_quantlib_reference import Contract, QdFpSurfacePricer, contracts + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SCALE = 10**12 + + +def metric(values: list[float]) -> dict[str, float | int]: + data = np.sort(np.abs(np.asarray(values, dtype=np.float64))) + return { + "count": int(data.size), + "median": float(np.quantile(data, 0.50)), + "p95": float(np.quantile(data, 0.95)), + "p99": float(np.quantile(data, 0.99)), + "max": float(data[-1]), + "mean": float(np.mean(data)), + } + + +def scaled(value: float) -> int: + return round(value * SCALE) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--batch-binary", + type=pathlib.Path, + default=ROOT / "target/release/examples/american_kbi_batch", + ) + parser.add_argument( + "--report", + type=pathlib.Path, + default=ROOT / "benchmark/american_kbi_runtime_accuracy_report.json", + ) + parser.add_argument("--moneyness-points", type=int, default=33) + parser.add_argument("--contract-seed", type=lambda value: int(value, 0)) + parser.add_argument("--contract-count", type=int) + args = parser.parse_args() + + selected = contracts(0x51D3, 24) + contract_source = "deterministic held-out sample seed 0x51d3" + if args.contract_seed is not None: + if args.contract_count is None: + raise SystemExit("--contract-seed requires --contract-count") + selected = contracts(args.contract_seed, args.contract_count) + contract_source = f"deterministic sample seed {args.contract_seed:#x}" + + grid = np.exp(np.linspace(-0.75, 0.75, args.moneyness_points)) + pricer = QdFpSurfacePricer() + lines: list[str] = [] + truth: list[tuple[str, int, float, float]] = [] + strike = 100.0 + for contract_index, contract in enumerate(selected): + maturity = contract.days / 365.0 + for kind, is_call in (("call", True), ("put", False)): + qdfp = strike * pricer.surface(grid, contract, contract.days, is_call) + for normalized_spot, exact in zip(grid, qdfp): + lines.append(" ".join([ + kind, + str(scaled(strike * normalized_spot)), + str(scaled(strike)), + str(scaled(contract.r)), + str(scaled(contract.q)), + str(scaled(contract.sigma)), + str(scaled(maturity)), + ])) + truth.append((kind, contract_index, float(normalized_spot), float(exact))) + + process = subprocess.run( + [str(args.batch_binary)], + input="\n".join(lines) + "\n", + text=True, + capture_output=True, + check=True, + ) + outputs = process.stdout.splitlines() + if len(outputs) != len(truth): + raise RuntimeError(f"runtime returned {len(outputs)} rows for {len(truth)} inputs") + + errors: dict[str, list[float]] = {"call": [], "put": []} + worst: dict[str, dict[str, object] | None] = {"call": None, "put": None} + rows = [asdict_contract(contract) for contract in selected] + for output, (kind, contract_index, normalized_spot, exact) in zip(outputs, truth): + if output.startswith("ERR:"): + raise RuntimeError(f"runtime error for {kind}/{contract_index}: {output}") + actual = int(output) / SCALE + residual = actual - exact + absolute = abs(residual) + errors[kind].append(residual) + if worst[kind] is None or absolute > worst[kind]["absolute_error_dollars"]: + worst[kind] = { + "contract_index": contract_index, + "contract": rows[contract_index], + "normalized_spot": normalized_spot, + "runtime_price_dollars": actual, + "qdfp_price_dollars": exact, + "signed_error_dollars": residual, + "absolute_error_dollars": absolute, + } + + digest_line = next( + line for line in (ROOT / "src/american_kbi_data.rs").read_text().splitlines() + if line.startswith("// SHA-256:") + ) + report = { + "method": "compiled Rust Q40 Kim Boundary Integration versus QuantLib QdFp accurateScheme", + "runtime_design": "18-node sqrt-time boundary; six-node singularity-cancelled Gaussian history; nine-node QdFp-regularized empirical premium cubature; log-boundary interpolation; all parameter-dependent work on-chain", + "quantlib_version": ql.__version__, + "artifact_sha256": digest_line.split(":", 1)[1].strip(), + "contract_source": contract_source, + "contract_count": len(selected), + "moneyness_points_per_contract": args.moneyness_points, + "validation_log_moneyness": [-0.75, 0.75], + "price_comparisons_per_leg": len(errors["call"]), + "absolute_error_dollars_at_100_strike": { + kind: metric(values) for kind, values in errors.items() + }, + "worst_cases": worst, + } + args.report.write_text(json.dumps(report, indent=2) + "\n") + print(json.dumps(report["absolute_error_dollars_at_100_strike"], indent=2)) + print(f"report={args.report}") + + +def asdict_contract(contract: Contract) -> dict[str, float | int]: + return { + "r": contract.r, + "q": contract.q, + "sigma": contract.sigma, + "days": contract.days, + } + + +if __name__ == "__main__": + main() diff --git a/scripts/validate_asian_corpora.py b/scripts/validate_asian_corpora.py new file mode 100644 index 0000000..b70a297 --- /dev/null +++ b/scripts/validate_asian_corpora.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Run the compiled Asian/TWAP pricer over the retained 100K/10K corpora.""" + +from __future__ import annotations + +import argparse +import json +import statistics +import subprocess +from pathlib import Path + + +SCALE = 10**12 +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_BINARY = ROOT / "target/release/examples/asian_batch" +DEFAULT_FILES = ( + ROOT / "benchmark/prod_asian_vectors.json", + ROOT / "benchmark/adv_asian_vectors.json", +) +INPUT_KEYS = ( + "s", + "k", + "r", + "q", + "sigma", + "t", + "averaging_time", + "fixed_average", + "fixed_weight", +) +EXPECTED_KEYS = ( + "expected_call", + "expected_put", + "expected_mean", + "expected_log_variance", +) + + +def percentile(values: list[int], percent: int) -> int: + return values[min(len(values) - 1, (len(values) * percent + 99) // 100 - 1)] + + +def validate(path: Path, binary: Path) -> dict: + payload = json.loads(path.read_text()) + vectors = payload["vectors"] + process = subprocess.run( + [str(binary)], + input="\n".join(" ".join(row[key] for key in INPUT_KEYS) for row in vectors) + "\n", + text=True, + capture_output=True, + check=True, + ) + lines = process.stdout.splitlines() + if len(lines) != len(vectors): + raise RuntimeError(f"{path.name}: got {len(lines)} outputs for {len(vectors)} vectors") + + deviations = [[] for _ in EXPECTED_KEYS] + errors = [] + categories: dict[str, dict[str, int]] = {} + max_cases: dict[str, dict | None] = {key: None for key in EXPECTED_KEYS} + for index, (row, line) in enumerate(zip(vectors, lines)): + if line.startswith("ERR"): + errors.append({"index": index, "category": row["category"], "error": line}) + continue + actual = list(map(int, line.split())) + expected = [int(row[key]) for key in EXPECTED_KEYS] + category = row["category"] + category_max = categories.setdefault(category, {key: 0 for key in EXPECTED_KEYS}) + for output_index, key in enumerate(EXPECTED_KEYS): + difference = abs(actual[output_index] - expected[output_index]) + deviations[output_index].append(difference) + category_max[key] = max(category_max[key], difference) + current_max = max_cases[key] + if current_max is None or difference > current_max["difference_raw"]: + max_cases[key] = { + "index": index, + "category": category, + "difference_raw": difference, + "actual_raw": actual[output_index], + "expected_raw": expected[output_index], + "inputs": {input_key: row[input_key] for input_key in INPUT_KEYS}, + } + + metrics = {} + for key, values in zip(EXPECTED_KEYS, deviations): + values.sort() + metrics[key.removeprefix("expected_")] = { + "median_raw": int(statistics.median(values)) if values else 0, + "p95_raw": percentile(values, 95) if values else 0, + "p99_raw": percentile(values, 99) if values else 0, + "max_raw": values[-1] if values else 0, + "max_real": (values[-1] / SCALE) if values else 0, + } + return { + "file": str(path.relative_to(ROOT)), + "reference": payload["meta"]["reference"], + "vectors": len(vectors), + "accepted": len(vectors) - len(errors), + "errors": errors[:20], + "error_count": len(errors), + "metrics": metrics, + "max_cases": max_cases, + "category_max_raw": categories, + } + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--binary", type=Path, default=DEFAULT_BINARY) + parser.add_argument("--report", type=Path, default=ROOT / "benchmark/asian_accuracy_report.json") + parser.add_argument("vectors", type=Path, nargs="*", default=list(DEFAULT_FILES)) + args = parser.parse_args() + + if not args.binary.exists(): + subprocess.run( + [ + "cargo", + "build", + "--release", + "--no-default-features", + "--features", + "asian", + "--example", + "asian_batch", + ], + cwd=ROOT, + check=True, + ) + report = { + "binary": str(args.binary.relative_to(ROOT)), + "scale": SCALE, + "corpora": [validate(path, args.binary) for path in args.vectors], + } + report["total_vectors"] = sum(corpus["vectors"] for corpus in report["corpora"]) + report["total_errors"] = sum(corpus["error_count"] for corpus in report["corpora"]) + rendered = json.dumps(report, indent=2, sort_keys=True) + "\n" + args.report.write_text(rendered) + print(rendered, end="") + if report["total_errors"]: + raise SystemExit("compiled Asian pricer rejected retained corpus vectors") + + +if __name__ == "__main__": + main() diff --git a/scripts/validate_asian_runtime.py b/scripts/validate_asian_runtime.py new file mode 100644 index 0000000..df2e630 --- /dev/null +++ b/scripts/validate_asian_runtime.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +"""Cross-check the compiled Asian/TWAP path against high-precision moments.""" + +from __future__ import annotations + +import argparse +import json +import math +import random +import subprocess +from pathlib import Path + +import mpmath as mp + +SCALE = 10**12 +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_BINARY = ROOT / "target/release/examples/asian_batch" + + +def round_raw(value: mp.mpf) -> int: + return int(mp.floor(value * SCALE + mp.mpf("0.5"))) + + +def reference(values: list[int]) -> list[int]: + spot, strike, rate, yield_rate, sigma, time, window, fixed_average, weight = [ + mp.mpf(value) / SCALE for value in values + ] + carry = rate - yield_rate + start = time - window + + def phi1(value: mp.mpf) -> mp.mpf: + return mp.expm1(value) / value if value else mp.mpf(1) + + b_window = carry * window + variance_window = sigma * sigma * window + future_mean = spot * mp.exp(carry * start) * phi1(b_window) + if b_window: + second_kernel = 2 / b_window * ( + mp.exp(b_window) * phi1(b_window + variance_window) + - phi1(2 * b_window + variance_window) + ) + elif variance_window: + second_kernel = ( + 2 * (mp.expm1(variance_window) - variance_window) / variance_window**2 + ) + else: + second_kernel = mp.mpf(1) + future_second = ( + spot**2 + * mp.exp((2 * carry + sigma**2) * start) + * second_kernel + ) + + mean = weight * fixed_average + (1 - weight) * future_mean + variance = (1 - weight) ** 2 * (future_second - future_mean**2) + log_variance = mp.log1p(variance / mean**2) + discount = mp.exp(-rate * time) + if log_variance: + root_variance = mp.sqrt(log_variance) + d1 = (mp.log(mean / strike) + log_variance / 2) / root_variance + d2 = d1 - root_variance + normal = lambda value: mp.erfc(-value / mp.sqrt(2)) / 2 + call = discount * (mean * normal(d1) - strike * normal(d2)) + else: + call = discount * max(mean - strike, 0) + put = call - discount * (mean - strike) + return [round_raw(call), round_raw(put), round_raw(mean), round_raw(log_variance)] + + +def raw(value: float) -> int: + return round(float(value) * SCALE) + + +def vectors(count: int) -> list[list[int]]: + rng = random.Random(0xA51A2026) + result = [] + for index in range(count): + spot = rng.uniform(20, 500) + strike = rng.uniform(0.5 * spot, 1.5 * spot) + rate = rng.uniform(0, 0.2) + yield_rate = rng.uniform(0, 0.2) + sigma = rng.uniform(0.05, 2) + time = rng.uniform(1 / 365, 2) + window = rng.uniform(min(time, 1 / (365 * 24)), time) + if index % 5 == 0: + window = min(time, 30 / (365 * 24 * 60)) + weight = 0 if index % 3 == 0 else rng.uniform(0.01, 0.95) + fixed_average = 0 if weight == 0 else rng.uniform(0.7 * spot, 1.3 * spot) + result.append( + [ + raw(spot), + raw(strike), + raw(rate), + raw(yield_rate), + raw(sigma), + raw(time), + raw(window), + raw(fixed_average), + raw(weight), + ] + ) + return result + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--cases", type=int, default=500) + parser.add_argument("--binary", type=Path, default=DEFAULT_BINARY) + parser.add_argument("--report", type=Path) + args = parser.parse_args() + + mp.mp.dps = 60 + if not args.binary.exists(): + subprocess.run( + [ + "cargo", + "build", + "--release", + "--no-default-features", + "--features", + "asian", + "--example", + "asian_batch", + ], + cwd=ROOT, + check=True, + ) + + inputs = vectors(args.cases) + process = subprocess.run( + [str(args.binary)], + input="\n".join(" ".join(map(str, row)) for row in inputs) + "\n", + text=True, + capture_output=True, + check=True, + ) + lines = process.stdout.splitlines() + if len(lines) != len(inputs): + raise RuntimeError(f"runtime returned {len(lines)} rows for {len(inputs)} inputs") + + maxima = [0, 0, 0, 0] + rejected = 0 + for values, line in zip(inputs, lines): + if line.startswith("ERR"): + rejected += 1 + continue + actual = list(map(int, line.split())) + expected = reference(values) + for index, (got, want) in enumerate(zip(actual, expected)): + maxima[index] = max(maxima[index], abs(got - want)) + + report = { + "seed": "0xA51A2026", + "mpmath_dps": mp.mp.dps, + "cases": args.cases, + "accepted": args.cases - rejected, + "rejected": rejected, + "max_abs_raw": dict(zip(("call", "put", "mean", "log_variance"), maxima)), + "max_abs_real": dict( + zip( + ("call", "put", "mean", "log_variance"), + [value / SCALE for value in maxima], + ) + ), + } + rendered = json.dumps(report, indent=2, sort_keys=True) + print(rendered) + if args.report: + args.report.write_text(rendered + "\n") + + if rejected or maxima[0] > 10_000 or maxima[1] > 10_000 or maxima[2] > 100: + raise SystemExit("Asian runtime validation exceeded its numerical budget") + + +if __name__ == "__main__": + main() diff --git a/scripts/validate_nig_runtime.py b/scripts/validate_nig_runtime.py new file mode 100644 index 0000000..66c9c7c --- /dev/null +++ b/scripts/validate_nig_runtime.py @@ -0,0 +1,496 @@ +#!/usr/bin/env python3 +"""Reproducible accuracy campaign for the actual fixed-point NIG runtime. + +The primary reference uses the exact Esscher-shift identity with SciPy's NIG +CDF. Upper tails are evaluated by reflection rather than `1 - cdf`, avoiding +catastrophic cancellation. A smaller audit set can additionally be checked +against arbitrary-precision direct density integration by +`scripts/nig_reference.py`. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import subprocess +import sys +from collections import Counter +from dataclasses import asdict, dataclass +from pathlib import Path + +import numpy as np +import scipy +from scipy.integrate import quad +from scipy.special import k1e +from scipy.stats import norminvgauss + + +SCALE = 10**12 +ROOT = Path(__file__).resolve().parents[1] +BINARY = ROOT / "target/release/examples/nig_batch" + + +@dataclass(frozen=True) +class Quote: + spot: int + strike: int + rate: int + dividend: int + time: int + alpha: int + beta: int + delta: int + requested: int + + def line(self) -> str: + return ( + f"{self.spot} {self.strike} {self.rate} {self.dividend} " + f"{self.time} {self.alpha} {self.beta} {self.delta} {self.requested}" + ) + + +def raw(value: float) -> int: + return int(round(value * SCALE)) + + +def clamp(value: float, low: float, high: float) -> float: + return min(max(value, low), high) + + +def make_quote( + spot: float, + strike: float, + rate: float, + dividend: float, + time: float, + alpha: float, + beta: float, + delta: float, + request_relative: float, +) -> Quote: + values = [spot, strike, time, alpha, delta] + if not all(math.isfinite(value) and value > 0 for value in values): + raise ValueError("invalid generated NIG quote") + q = Quote( + raw(spot), + raw(strike), + raw(rate), + raw(dividend), + raw(time), + raw(alpha), + raw(beta), + raw(delta), + max(1, raw(max(spot, strike) * request_relative)), + ) + return q + + +def production_quotes(count: int, seed: int) -> list[Quote]: + rng = np.random.default_rng(seed) + quotes: list[Quote] = [] + while len(quotes) < count: + time = math.exp(rng.uniform(math.log(1 / 3650), math.log(5.0))) + alpha = math.exp(rng.uniform(math.log(2.05), math.log(100.0))) + # Sample beta from the intersection of the two declared 0.65-alpha + # headroom constraints. Ten percent of quotes concentrate near a gate. + lo = max(-0.65 * alpha, -1.0 - 0.65 * alpha) + hi = min(0.65 * alpha, -1.0 + 0.65 * alpha) + if rng.random() < 0.10: + edge = lo if rng.random() < 0.5 else hi + beta = edge + (hi - lo) * rng.uniform(1e-6, 2e-3) * (1 if edge == lo else -1) + else: + beta = rng.uniform(lo, hi) + + gamma = math.sqrt(alpha * alpha - beta * beta) + annual_sigma = math.exp(rng.uniform(math.log(0.03), math.log(1.5))) + delta = annual_sigma * annual_sigma * gamma**3 / (alpha * alpha) + # Keep both delta/year and elapsed delta inside the executable domain. + delta = clamp(delta, 1e-3 / time * (1 + 1e-5), 15.0 * (1 - 1e-8)) + if delta * time > 15.0: + delta = 15.0 / time * (1 - 1e-8) + if not (0 < delta <= 15 and 1e-3 <= delta * time <= 15): + continue + + rate = rng.uniform(-0.25, 0.25) + dividend = rng.uniform(-0.25, 0.25) + if rng.random() < 0.10: + magnitude = rng.uniform(1.8, 1.995) + log_forward = magnitude if rng.random() < 0.5 else -magnitude + else: + log_forward = rng.triangular(-1.8, 0.0, 1.8) + spot = math.exp(rng.uniform(math.log(1.0), math.log(1_000.0))) + log_moneyness = log_forward - (rate - dividend) * time + strike = spot / math.exp(log_moneyness) + if not (1e-6 < strike <= 100_000 and spot <= 100_000): + continue + quotes.append( + make_quote( + spot, + strike, + rate, + dividend, + time, + alpha, + beta, + delta, + 5e-5, + ) + ) + return quotes + + +def adversarial_quotes(count: int, seed: int) -> list[Quote]: + rng = np.random.default_rng(seed) + quotes: list[Quote] = [] + times = np.array([1e-5, 1e-4, 1e-3, 1e-2, 0.1, 1.0, 4.999999]) + alphas = np.array([2.000001, 2.01, 3.0, 10.0, 99.99, 99.999999]) + log_forwards = np.array([-1.999999, -1.99, -1.0, -1e-8, 0.0, 1e-8, 1.0, 1.99, 1.999999]) + boundary_rates = np.array([-0.249999, -0.20, 0.0, 0.20, 0.249999]) + + while len(quotes) < count: + time = float(rng.choice(times)) + alpha = float(rng.choice(alphas)) + lo = max(-0.65 * alpha, -1.0 - 0.65 * alpha) + hi = min(0.65 * alpha, -1.0 + 0.65 * alpha) + if rng.random() < 0.75: + epsilon = (hi - lo) * 10 ** rng.uniform(-9, -4) + beta = lo + epsilon if rng.random() < 0.5 else hi - epsilon + else: + beta = rng.uniform(lo, hi) + + max_elapsed = min(15.0, 15.0 * time) + if max_elapsed < 1e-3: + continue + elapsed_choices = [1.000001e-3, max_elapsed * (1 - 1e-8)] + if max_elapsed > 1.01e-3: + elapsed_choices.append( + math.exp(rng.uniform(math.log(1.000001e-3), math.log(max_elapsed))) + ) + elapsed = float(rng.choice(elapsed_choices)) + delta = elapsed / time + if not (0 < delta <= 15): + continue + + rate = float(rng.choice(boundary_rates)) + dividend = float(rng.choice(boundary_rates)) + log_forward = float(rng.choice(log_forwards)) + spot = float(10 ** rng.uniform(-2, 4)) + strike = spot / math.exp(log_forward - (rate - dividend) * time) + if not (1e-9 < strike <= 100_000 and spot <= 100_000): + continue + quotes.append( + make_quote( + spot, + strike, + rate, + dividend, + time, + alpha, + beta, + delta, + 1e-4, + ) + ) + return quotes + + +def fixed_runtime(quotes: list[Quote]) -> list[tuple[str, ...]]: + payload = "\n".join(quote.line() for quote in quotes) + "\n" + process = subprocess.run( + [str(BINARY)], + input=payload, + text=True, + capture_output=True, + check=True, + ) + lines = process.stdout.splitlines() + if len(lines) != len(quotes): + raise RuntimeError(f"runtime returned {len(lines)} lines for {len(quotes)} quotes") + return [tuple(line.split()) for line in lines] + + +def direct_density_reference(quote: Quote) -> tuple[float, float]: + """Adaptive direct-OTM fallback for CDF cancellation/slow convergence.""" + spot, strike, rate, dividend, time, alpha, beta, delta_py = ( + value / SCALE + for value in ( + quote.spot, + quote.strike, + quote.rate, + quote.dividend, + quote.time, + quote.alpha, + quote.beta, + quote.delta, + ) + ) + elapsed = delta_py * time + gamma = math.sqrt(alpha * alpha - beta * beta) + gamma_one = math.sqrt(alpha * alpha - (beta + 1) ** 2) + kappa = math.log(spot / strike) + (rate - dividend) * time + elapsed * ( + gamma_one - gamma + ) + threshold = -kappa + discounted_spot = spot * math.exp(-dividend * time) + discounted_strike = strike * math.exp(-rate * time) + call_is_otm = discounted_spot <= discounted_strike + + def integrand(y: float) -> float: + if y <= 0: + return 0.0 + x = threshold + y if call_is_otm else threshold - y + omega = math.hypot(elapsed, x) + z = alpha * omega + log_value = ( + math.log(alpha * elapsed / (math.pi * omega)) + + math.log(float(k1e(z))) + + elapsed * gamma + + beta * x + - z + ) + if call_is_otm: + log_value += ( + math.log(math.expm1(y)) + if y < 50 + else y + math.log1p(-math.exp(-y)) + ) + else: + log_value += math.log(-math.expm1(-y)) + if log_value < -745: + return 0.0 + return math.exp(log_value) + + integral, _ = quad(integrand, 0.0, np.inf, epsabs=1e-13, epsrel=2e-12, limit=300) + otm = discounted_strike * integral + if call_is_otm: + return otm, otm + discounted_strike - discounted_spot + return otm + discounted_spot - discounted_strike, otm + + +def reference_prices(quotes: list[Quote]) -> tuple[np.ndarray, np.ndarray, int]: + matrix = np.array( + [ + [ + q.spot, + q.strike, + q.rate, + q.dividend, + q.time, + q.alpha, + q.beta, + q.delta, + ] + for q in quotes + ], + dtype=np.float64, + ) / SCALE + spot, strike, rate, dividend, time, alpha, beta, delta_py = matrix.T + elapsed = delta_py * time + gamma = np.sqrt(alpha * alpha - beta * beta) + gamma_one = np.sqrt(alpha * alpha - (beta + 1.0) ** 2) + kappa = np.log(spot / strike) + (rate - dividend) * time + elapsed * (gamma_one - gamma) + threshold = -kappa + discounted_spot = spot * np.exp(-dividend * time) + discounted_strike = strike * np.exp(-rate * time) + scipy_a = alpha * elapsed + scipy_b = beta * elapsed + scipy_b_one = (beta + 1.0) * elapsed + + call_is_otm = discounted_spot <= discounted_strike + call = np.empty(len(quotes), dtype=np.float64) + put = np.empty(len(quotes), dtype=np.float64) + + call_indices = np.flatnonzero(call_is_otm) + if len(call_indices): + i = call_indices + # NIG symmetry: P_beta[X > h] = P_-beta[X < -h]. This avoids 1-CDF. + tail = norminvgauss.cdf( + -threshold[i], scipy_a[i], -scipy_b[i], scale=elapsed[i] + ) + tail_one = norminvgauss.cdf( + -threshold[i], scipy_a[i], -scipy_b_one[i], scale=elapsed[i] + ) + call[i] = discounted_spot[i] * tail_one - discounted_strike[i] * tail + put[i] = call[i] + discounted_strike[i] - discounted_spot[i] + + put_indices = np.flatnonzero(~call_is_otm) + if len(put_indices): + i = put_indices + lower = norminvgauss.cdf( + threshold[i], scipy_a[i], scipy_b[i], scale=elapsed[i] + ) + lower_one = norminvgauss.cdf( + threshold[i], scipy_a[i], scipy_b_one[i], scale=elapsed[i] + ) + put[i] = discounted_strike[i] * lower - discounted_spot[i] * lower_one + call[i] = put[i] + discounted_spot[i] - discounted_strike[i] + + # Roundoff in a difference of two positive digital legs may produce a + # sub-nanodollar negative number. Anything material is retained as a + # reference failure rather than hidden. + call[(call < 0) & (call > -1e-9)] = 0.0 + put[(put < 0) & (put > -1e-9)] = 0.0 + failed = ~np.isfinite(call) | ~np.isfinite(put) | (call < -1e-9) | (put < -1e-9) + fallback_count = int(np.sum(failed)) + for index in np.flatnonzero(failed): + call[index], put[index] = direct_density_reference(quotes[int(index)]) + return call, put, fallback_count + + +def percentile(values: list[float], quantile: float) -> float | None: + if not values: + return None + return float(np.quantile(np.asarray(values), quantile)) + + +def evaluate(name: str, quotes: list[Quote]) -> dict: + digest = hashlib.sha256(("\n".join(q.line() for q in quotes) + "\n").encode()).hexdigest() + runtime = fixed_runtime(quotes) + accepted_indices = [ + index for index, result in enumerate(runtime) if result and result[0] == "OK" + ] + accepted_quotes = [quotes[index] for index in accepted_indices] + reference_call, reference_put, reference_fallbacks = reference_prices(accepted_quotes) + errors: list[float] = [] + normalized_errors: list[float] = [] + certificate_ratios: list[float] = [] + reject_reasons: Counter[str] = Counter() + tiers: Counter[int] = Counter() + certificate_violations = 0 + request_violations = 0 + reference_failures = 0 + worst: dict | None = None + + reference_index = 0 + for index, (quote, result) in enumerate(zip(quotes, runtime)): + if not result or result[0] != "OK": + reject_reasons[" ".join(result) if result else "ERR:empty"] += 1 + continue + call_ref = reference_call[reference_index] + put_ref = reference_put[reference_index] + reference_index += 1 + if ( + not np.isfinite(call_ref) + or not np.isfinite(put_ref) + or call_ref < -1e-9 + or put_ref < -1e-9 + ): + reference_failures += 1 + continue + call = int(result[1]) / SCALE + put = int(result[2]) / SCALE + certificate = int(result[3]) / SCALE + tier = int(result[4]) + tiers[tier] += 1 + error = max(abs(call - call_ref), abs(put - put_ref)) + errors.append(error) + normalized_errors.append(error / (max(quote.spot, quote.strike) / SCALE) * 100) + certificate_ratios.append(error / certificate if certificate else math.inf) + # Allow five raw output units for conversion/reference rounding. + if error > certificate + 5 / SCALE: + certificate_violations += 1 + if error > quote.requested / SCALE + 5 / SCALE: + request_violations += 1 + if worst is None or error > worst["max_call_put_abs_error"]: + worst = { + "index": index, + "input": asdict(quote), + "runtime_call": call, + "runtime_put": put, + "reference_call": float(call_ref), + "reference_put": float(put_ref), + "returned_max_abs_error": certificate, + "max_call_put_abs_error": error, + "tier": tier, + } + + accepted = len(errors) + return { + "name": name, + "seeded_input_sha256": digest, + "quotes": len(quotes), + "accepted": accepted, + "acceptance_rate": accepted / len(quotes), + "rejected": sum(reject_reasons.values()), + "reference_failures": reference_failures, + "reference_fallbacks": reference_fallbacks, + "reject_reasons": dict(reject_reasons), + "tiers": {str(key): value for key, value in sorted(tiers.items())}, + "certificate_violations": certificate_violations, + "request_violations": request_violations, + "absolute_error": { + "median": percentile(errors, 0.5), + "p90": percentile(errors, 0.9), + "p99": percentile(errors, 0.99), + "p999": percentile(errors, 0.999), + "max": max(errors) if errors else None, + }, + "absolute_error_per_100_notional": { + "median": percentile(normalized_errors, 0.5), + "p90": percentile(normalized_errors, 0.9), + "p99": percentile(normalized_errors, 0.99), + "p999": percentile(normalized_errors, 0.999), + "max": max(normalized_errors) if normalized_errors else None, + }, + "error_over_returned_allowance": { + "p99": percentile(certificate_ratios, 0.99), + "max": max(certificate_ratios) if certificate_ratios else None, + }, + "worst_accepted_quote": worst, + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--production", type=int, default=100_000) + parser.add_argument("--adversarial", type=int, default=10_000) + parser.add_argument("--seed", type=int, default=0x4E49475F2026) + parser.add_argument( + "--output", type=Path, default=ROOT / "benchmark/nig_release_report.json" + ) + parser.add_argument("--skip-build", action="store_true") + args = parser.parse_args() + + if not args.skip_build: + subprocess.run( + [ + "cargo", + "build", + "--release", + "--no-default-features", + "--features", + "nig", + "--example", + "nig_batch", + ], + cwd=ROOT, + check=True, + ) + + production = production_quotes(args.production, args.seed) + adversarial = adversarial_quotes(args.adversarial, args.seed ^ 0xA5A5A5A5) + report = { + "schema": 1, + "model": "exponential NIG with martingale correction and beta+1 Esscher shift", + "runtime": "SolMath fixed-point direct OTM Gauss-Kronrod 15/7", + "reference": "SciPy norminvgauss CDF Esscher identity; upper tails by NIG reflection", + "versions": { + "python": sys.version.split()[0], + "numpy": np.__version__, + "scipy": scipy.__version__, + }, + "scale": SCALE, + "seed": args.seed, + "production": evaluate("production", production), + "adversarial": evaluate("adversarial", adversarial), + } + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(report, indent=2) + "\n") + print(json.dumps(report, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify_critical_invariants.sh b/scripts/verify_critical_invariants.sh new file mode 100755 index 0000000..0f4699b --- /dev/null +++ b/scripts/verify_critical_invariants.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$repo_root/target/critical-invariants}" + +cargo test --offline --test critical_invariants --no-default-features +cargo test --offline --test critical_invariants --all-features +RUSTFLAGS="-C overflow-checks=on" cargo test --offline --release --test critical_invariants --all-features +RUSTFLAGS="-C overflow-checks=off" cargo test --offline --release --test critical_invariants --all-features diff --git a/scripts/verify_feature_contract.py b/scripts/verify_feature_contract.py new file mode 100755 index 0000000..f74045b --- /dev/null +++ b/scripts/verify_feature_contract.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Fail release checks when SolMath's public Cargo feature contract drifts.""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +EXPECTED_VERSION = "0.2.0" +EXPECTED_FEATURES = { + "american-kbi": ["transcendental"], + "asian": ["transcendental"], + "barrier": ["transcendental"], + "bivariate": ["transcendental"], + "bs": ["transcendental"], + "complex": ["transcendental"], + "default": ["transcendental"], + "full": [ + "transcendental", + "complex", + "bs", + "iv", + "barrier", + "asian", + "nig", + "heston", + "sabr", + "pool", + "bivariate", + "american-kbi", + "rainbow", + ], + "heston": ["bs"], + "iv": ["bs"], + "nig": ["transcendental"], + "pade-iv": ["iv"], + "pool": ["transcendental"], + "rainbow": ["bivariate"], + "sabr": ["transcendental"], + "table-gen": ["bivariate"], + "transcendental": [], +} + + +def metadata() -> dict: + result = subprocess.run( + ["cargo", "metadata", "--no-deps", "--format-version", "1"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ) + packages = json.loads(result.stdout)["packages"] + return next(package for package in packages if package["name"] == "solmath") + + +def source_features() -> set[str]: + pattern = re.compile(r'feature\s*=\s*"([^"]+)"') + found: set[str] = set() + for source in (ROOT / "src").rglob("*.rs"): + found.update(pattern.findall(source.read_text(encoding="utf-8"))) + return found + + +def verify(package: dict) -> None: + errors: list[str] = [] + actual_features = package["features"] + + if package["version"] != EXPECTED_VERSION: + errors.append( + f"release version is {package['version']!r}; expected {EXPECTED_VERSION!r}" + ) + if package["dependencies"]: + errors.append("the published library must remain dependency-free") + if set(actual_features) != set(EXPECTED_FEATURES): + missing = sorted(set(EXPECTED_FEATURES) - set(actual_features)) + extra = sorted(set(actual_features) - set(EXPECTED_FEATURES)) + errors.append(f"feature names drifted (missing={missing}, extra={extra})") + + for feature, expected_dependencies in EXPECTED_FEATURES.items(): + actual_dependencies = actual_features.get(feature) + if actual_dependencies is not None and set(actual_dependencies) != set( + expected_dependencies + ): + errors.append( + f"feature {feature!r} expands to {actual_dependencies}; " + f"expected {expected_dependencies}" + ) + + undeclared = source_features() - set(actual_features) + if undeclared: + errors.append(f"source uses undeclared Cargo features: {sorted(undeclared)}") + + full = set(actual_features.get("full", [])) + for excluded in ("table-gen", "pade-iv"): + if excluded in full: + errors.append(f"offline/experimental feature {excluded!r} leaked into 'full'") + if "complex" in actual_features.get("default", []): + errors.append("complex arithmetic must not be linked by default") + + if errors: + raise SystemExit("feature contract failed:\n- " + "\n- ".join(errors)) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--list", + action="store_true", + help="print independently checkable feature names after validation", + ) + args = parser.parse_args() + package = metadata() + verify(package) + if args.list: + print("\n".join(sorted(package["features"]))) + else: + print("feature contract: ok") + + +if __name__ == "__main__": + main() diff --git a/src/american_kbi.rs b/src/american_kbi.rs new file mode 100644 index 0000000..0a067db --- /dev/null +++ b/src/american_kbi.rs @@ -0,0 +1,809 @@ +//! Fully on-chain American pricing by Kim Boundary Integration (KBI). +//! +//! The normalized American-put exercise boundary is reconstructed from +//! `(r, q, sigma, T)` with a fixed, singularity-cancelled Gaussian history +//! rule and a compact QdFp-regularized premium cubature. +//! Calls use exact American put-call duality. There is no live surface, matrix, +//! account, operator upload, trusted builder, or other off-chain input. + +use crate::american_kbi_data::*; +use crate::arithmetic::fp_sqrt; +use crate::constants::{SCALE, SCALE_I}; +use crate::error::SolMathError; +use crate::transcendental::{exp_fixed_i, ln_fixed_i}; + +/// Number of graded time nodes used to reconstruct the exercise boundary. +pub const AMERICAN_KBI_NODES: usize = KBI_NODES; +/// Number of quadrature points used for the final early-exercise premium. +pub const AMERICAN_KBI_PRICE_POINTS: usize = KBI_PRICE_POINTS; +/// SHA-256 of the generated, parameter-independent KBI geometry artifact. +pub const AMERICAN_KBI_ARTIFACT_SHA256: [u8; 32] = KBI_DATA_SHA256; + +const MAX_RATE: u128 = 120_000_000_000; +const MIN_SIGMA: u128 = 100_000_000_000; +const MAX_SIGMA: u128 = 1_200_000_000_000; +const MIN_MATURITY: u128 = 30 * SCALE / 365; +const MAX_MATURITY: u128 = 2 * SCALE; +// Certified quote domain: |ln(S/K)| <= 0.75. Calls reach this same guard +// through exact put-call duality, where the normalized ratio is K/S. +const MIN_NORMALIZED_SPOT_Q: i64 = 519_372_517_311; +const MAX_NORMALIZED_SPOT_Q: i64 = 2_327_666_134_268; +const MAX_SAFE_QUOTE: u128 = u128::MAX / Q_ONE as u128; + +// Internal arithmetic is Q40. Unlike the crate's public decimal SCALE, every +// normalized multiply is a single wide multiply followed by a constant shift. +const Q_BITS: u32 = 40; +const Q_ONE: i64 = 1i64 << Q_BITS; +const SCALE_TO_Q_RECIP_Q48: i128 = 309_485_009_821_345; +const PDF_ZERO_Q: i64 = 438_641_676_113; + +const BOUNDARY_FLOOR_Q: i64 = 1_099_512; // 1e-6 +const NEGLIGIBLE_PREMIUM_Q: i64 = 1_099_512; // rT <= 1e-6 +const FIRST_NODE_HIGH_SCALE_Q: i64 = 2 * Q_ONE; +const FIRST_NODE_LOW_SCALE_Q: i64 = 3 * Q_ONE / 4; +const THIRD_NODE_EXTRAPOLATION_Q: i64 = 37 * Q_ONE / 40; +const BOUNDARY_HIGH_SWITCH_Q: i64 = 95 * Q_ONE / 100; +const STEP_LOW_Q: i64 = 35 * Q_ONE / 100; +const STEP_HIGH_Q: i64 = 165 * Q_ONE / 100; + +const EXP_C0_Q: i64 = Q_ONE; +const EXP_C1_Q: i64 = Q_ONE; +const EXP_C2_Q: i64 = 549_755_813_888; +const EXP_C3_Q: i64 = 183_251_937_963; +const EXP_C4_Q: i64 = 45_812_984_491; +const EXP_C5_Q: i64 = 9_162_596_898; +const EXP_C6_Q: i64 = 1_527_099_483; + +/// American option leg priced by [`american_kbi_price`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AmericanKbiKind { + /// American call, evaluated through exact call-put duality. + Call, + /// American put. + Put, +} + +#[derive(Clone, Copy)] +struct Parameters { + rate: i64, + yield_rate: i64, + sigma_sqrt_maturity: i64, + inverse_sigma_sqrt_maturity: i64, + sqrt_maturity_over_sigma: i64, + rate_maturity: i64, + yield_maturity: i64, + drift_maturity: i64, +} + +struct ExerciseBoundary { + values: [i64; KBI_NODES + 1], + log_values: [i64; KBI_NODES + 1], +} + +#[inline(always)] +fn round_shift(value: i128, bits: u32) -> i128 { + let half = 1i128 << (bits - 1); + if value >= 0 { + (value + half) >> bits + } else { + -((-value + half) >> bits) + } +} + +#[inline(always)] +fn qmul(a: i64, b: i64) -> i64 { + let value = (a as i128 * b as i128) >> Q_BITS; + debug_assert!(i64::try_from(value).is_ok()); + value as i64 +} + +#[inline(always)] +fn qdiv(a: i64, b: i64) -> Result { + if b == 0 { + return Err(SolMathError::DivisionByZero); + } + let value = ((a as i128) << Q_BITS) / b as i128; + i64::try_from(value).map_err(|_| SolMathError::Overflow) +} + +#[inline(always)] +fn scale_to_q(value: i128) -> Result { + let converted = round_shift(value * SCALE_TO_Q_RECIP_Q48, 48); + i64::try_from(converted).map_err(|_| SolMathError::Overflow) +} + +#[inline(always)] +fn q_to_scale(value: i64) -> i128 { + round_shift(value as i128 * SCALE_I, Q_BITS) +} + +#[inline(always)] +fn qln(value: i64) -> Result { + if value <= 0 { + return Err(SolMathError::DomainError); + } + scale_to_q(ln_fixed_i(q_to_scale(value) as u128)?) +} + +#[inline(never)] +fn qsqrt(value: i64) -> Result { + if value <= 0 { + return Err(SolMathError::DomainError); + } + scale_to_q(fp_sqrt(q_to_scale(value) as u128)? as i128) +} + +#[inline(never)] +fn qexp(value: i64) -> Result { + scale_to_q(exp_fixed_i(q_to_scale(value))?) +} + +#[inline(always)] +fn boundary_cdf_and_pdf(value: i64) -> (i64, i64) { + const STEP_BITS: u32 = Q_BITS - 3; + // At six standard deviations the omitted probability is below 1e-9, + // far under the boundary kernel's 5.5e-6 certified interpolation error. + // Saturating there also removes sub-unit coefficient wobble in the tail. + const LIMIT: i64 = 6 * Q_ONE; + if value <= -LIMIT { + return (0, 0); + } + if value >= LIMIT { + return (Q_ONE, 0); + } + + let absolute = value.abs(); + let index = (absolute >> STEP_BITS) as usize; + let interval_start = (index as i64) << STEP_BITS; + let coordinate = (absolute - interval_start) << 3; + let a = KBI_NORMAL_HERMITE_A[index]; + let b = KBI_NORMAL_HERMITE_B[index]; + let c = KBI_NORMAL_HERMITE_C[index]; + let d = KBI_NORMAL_HERMITE_D[index]; + let cdf_positive = (qmul(qmul(qmul(a, coordinate) + b, coordinate) + c, coordinate) + d) + .clamp(Q_ONE / 2, Q_ONE); + let coordinate_derivative = qmul(qmul(3 * a, coordinate) + 2 * b, coordinate) + c; + let pdf = (8 * coordinate_derivative).clamp(0, PDF_ZERO_Q); + ( + if value >= 0 { + cdf_positive + } else { + Q_ONE - cdf_positive + }, + pdf, + ) +} + +#[inline(always)] +fn premium_cdf(value: i64) -> i64 { + const STEP_BITS: u32 = Q_BITS - 3; + const LIMIT: i64 = 6 * Q_ONE; + if value <= -LIMIT { + return 0; + } + if value >= LIMIT { + return Q_ONE; + } + + let absolute = value.abs(); + let index = (absolute >> STEP_BITS) as usize; + let interval_start = (index as i64) << STEP_BITS; + let coordinate = (absolute - interval_start) << 3; + let a = KBI_NORMAL_HERMITE_A[index]; + let b = KBI_NORMAL_HERMITE_B[index]; + let c = KBI_NORMAL_HERMITE_C[index]; + let d = KBI_NORMAL_HERMITE_D[index]; + let cdf_positive = (qmul(qmul(qmul(a, coordinate) + b, coordinate) + c, coordinate) + d) + .clamp(Q_ONE / 2, Q_ONE); + if value >= 0 { + cdf_positive + } else { + Q_ONE - cdf_positive + } +} + +/// Sixth-order exponential on the live `[-0.24, 0]` discount domain. +#[inline(never)] +fn exp_small(value: i64) -> Result { + if !(-3 * Q_ONE / 10..=0).contains(&value) { + return Err(SolMathError::DomainError); + } + let polynomial = EXP_C5_Q + qmul(value, EXP_C6_Q); + let polynomial = EXP_C4_Q + qmul(value, polynomial); + let polynomial = EXP_C3_Q + qmul(value, polynomial); + let polynomial = EXP_C2_Q + qmul(value, polynomial); + let polynomial = EXP_C1_Q + qmul(value, polynomial); + Ok((EXP_C0_Q + qmul(value, polynomial)).max(0)) +} + +/// Degree-five discount kernel. On `[-0.24, 0]` the Taylor remainder is +/// bounded by `0.24^6 / 6! < 2.66e-7`; the European control value retains the +/// degree-six path above. +#[inline(always)] +fn exp_kernel(value: i64) -> Result { + if !(-3 * Q_ONE / 10..=0).contains(&value) { + return Err(SolMathError::DomainError); + } + let polynomial = EXP_C4_Q + qmul(value, EXP_C5_Q); + let polynomial = EXP_C3_Q + qmul(value, polynomial); + let polynomial = EXP_C2_Q + qmul(value, polynomial); + let polynomial = EXP_C1_Q + qmul(value, polynomial); + Ok((EXP_C0_Q + qmul(value, polynomial)).max(0)) +} + +fn parameters( + rate: u128, + dividend_yield: u128, + sigma: u128, + maturity: u128, +) -> Result { + if rate > MAX_RATE + || dividend_yield > MAX_RATE + || !(MIN_SIGMA..=MAX_SIGMA).contains(&sigma) + || !(MIN_MATURITY..=MAX_MATURITY).contains(&maturity) + || rate > i128::MAX as u128 + || dividend_yield > i128::MAX as u128 + || sigma > i128::MAX as u128 + || maturity > i128::MAX as u128 + { + return Err(SolMathError::DomainError); + } + + let rate = scale_to_q(rate as i128)?; + let yield_rate = scale_to_q(dividend_yield as i128)?; + let sigma = scale_to_q(sigma as i128)?; + let maturity = scale_to_q(maturity as i128)?; + let sqrt_maturity = qsqrt(maturity)?; + let sigma_sqrt_maturity = qmul(sigma, sqrt_maturity); + let sigma_squared = qmul(sigma, sigma); + let drift = rate + .checked_sub(yield_rate) + .and_then(|value| value.checked_add(sigma_squared / 2)) + .ok_or(SolMathError::Overflow)?; + + Ok(Parameters { + rate, + yield_rate, + sigma_sqrt_maturity, + inverse_sigma_sqrt_maturity: qdiv(Q_ONE, sigma_sqrt_maturity)?, + sqrt_maturity_over_sigma: qdiv(sqrt_maturity, sigma)?, + rate_maturity: qmul(rate, maturity), + yield_maturity: qmul(yield_rate, maturity), + drift_maturity: qmul(drift, maturity), + }) +} + +#[inline(never)] +fn european_put_normalized( + normalized_spot: i64, + parameters: Parameters, +) -> Result { + let log_spot = qln(normalized_spot)?; + let d1 = qmul( + log_spot + .checked_add(parameters.drift_maturity) + .ok_or(SolMathError::Overflow)?, + parameters.inverse_sigma_sqrt_maturity, + ); + let d2 = d1 + .checked_sub(parameters.sigma_sqrt_maturity) + .ok_or(SolMathError::Overflow)?; + let n_d1 = premium_cdf(-d1); + let n_d2 = premium_cdf(-d2); + let discounted_strike = qmul(exp_small(-parameters.rate_maturity)?, n_d2); + let discounted_spot = qmul( + normalized_spot, + qmul(exp_small(-parameters.yield_maturity)?, n_d1), + ); + Ok(discounted_strike + .checked_sub(discounted_spot) + .ok_or(SolMathError::Overflow)? + .clamp(0, Q_ONE)) +} + +#[inline(always)] +fn expiry_boundary(parameters: Parameters) -> Result { + if parameters.yield_rate > parameters.rate { + Ok(qdiv(parameters.rate, parameters.yield_rate)?.max(BOUNDARY_FLOOR_Q)) + } else { + Ok(Q_ONE) + } +} + +#[inline(never)] +fn boundary_residual( + index: usize, + candidate: i64, + log_boundary: &[i64; KBI_NODES + 1], + coefficient: &[i64; KBI_NODES + 1], + parameters: Parameters, + node_discount: i64, + boundary_discount: &[i64; KBI_BOUNDARY_ORDER], +) -> Result<(i64, i64), SolMathError> { + let time_fraction = KBI_TIME_FRACTION[index]; + let inverse_volatility_at_node = qmul( + parameters.inverse_sigma_sqrt_maturity, + KBI_NODES as i64 * Q_ONE / index as i64, + ); + let inverse_candidate = qdiv(Q_ONE, candidate)?; + let log_candidate = qln(candidate)?; + let drift_at_node = qmul(parameters.drift_maturity, time_fraction); + let european_d1 = qmul( + log_candidate + .checked_add(drift_at_node) + .ok_or(SolMathError::Overflow)?, + inverse_volatility_at_node, + ); + let (european_cdf, european_pdf) = boundary_cdf_and_pdf(-european_d1); + let mut residual = Q_ONE + .checked_sub(qmul(node_discount, european_cdf)) + .ok_or(SolMathError::Overflow)?; + let mut derivative = qmul( + qmul(node_discount, european_pdf), + qmul(inverse_candidate, inverse_volatility_at_node), + ); + + let mut regular_sum = 0i64; + let mut regular_derivative_sum = 0i64; + let mut singular_sum = 0i64; + let mut singular_derivative_sum = 0i64; + let rate_over_boundary = qmul(parameters.rate, inverse_candidate); + let rate_over_boundary_squared = qmul(rate_over_boundary, inverse_candidate); + let candidate_coefficient = parameters + .yield_rate + .checked_sub(rate_over_boundary) + .ok_or(SolMathError::Overflow)?; + for point in 0..KBI_BOUNDARY_ORDER { + let flat_index = (index - 1) * KBI_BOUNDARY_ORDER + point; + let lag_fraction = KBI_BOUNDARY_LAG_FRACTION[flat_index]; + let discount = boundary_discount[point]; + let left = KBI_BOUNDARY_LEFT[flat_index] as usize; + let fraction = KBI_BOUNDARY_FRACTION[flat_index]; + let right_log_boundary = if left + 1 == index { + log_candidate + } else { + log_boundary[left + 1] + }; + let log_boundary_sample = log_boundary[left] + .checked_add(qmul(right_log_boundary - log_boundary[left], fraction)) + .ok_or(SolMathError::Overflow)?; + let right_coefficient = if left + 1 == index { + candidate_coefficient + } else { + coefficient[left + 1] + }; + let coefficient_sample = coefficient[left] + .checked_add(qmul(right_coefficient - coefficient[left], fraction)) + .ok_or(SolMathError::Overflow)?; + let drift_lag = qmul(parameters.drift_maturity, lag_fraction); + let numerator = log_candidate + .checked_sub(log_boundary_sample) + .and_then(|value| value.checked_add(drift_lag)) + .ok_or(SolMathError::Overflow)?; + let inverse_volatility_lag = qmul( + parameters.inverse_sigma_sqrt_maturity, + KBI_BOUNDARY_INV_SQRT_LAG_FRACTION[flat_index], + ); + let d1 = qmul(numerator, inverse_volatility_lag); + let (cdf, pdf) = boundary_cdf_and_pdf(-d1); + let d_candidate = qmul( + qmul(inverse_candidate, inverse_volatility_lag), + KBI_BOUNDARY_CANDIDATE_LOG_FACTOR[flat_index], + ); + let discounted_pdf = qmul(discount, pdf); + let regular = qmul(discount, cdf); + let integration_weight = KBI_BOUNDARY_REGULAR_WEIGHT[flat_index]; + regular_sum = regular_sum + .checked_add(qmul(integration_weight, regular)) + .ok_or(SolMathError::Overflow)?; + regular_derivative_sum = regular_derivative_sum + .checked_add(qmul(integration_weight, -qmul(discounted_pdf, d_candidate))) + .ok_or(SolMathError::Overflow)?; + let coefficient_derivative = qmul( + rate_over_boundary_squared, + KBI_BOUNDARY_CANDIDATE_COEFFICIENT_FACTOR[flat_index], + ); + singular_sum = singular_sum + .checked_add(qmul( + KBI_BOUNDARY_SINGULAR_WEIGHT[flat_index], + qmul(discounted_pdf, coefficient_sample), + )) + .ok_or(SolMathError::Overflow)?; + let density_derivative = coefficient_derivative + .checked_sub(qmul(qmul(coefficient_sample, d1), d_candidate)) + .ok_or(SolMathError::Overflow)?; + singular_derivative_sum = singular_derivative_sum + .checked_add(qmul( + KBI_BOUNDARY_SINGULAR_WEIGHT[flat_index], + qmul(discounted_pdf, density_derivative), + )) + .ok_or(SolMathError::Overflow)?; + } + + residual = residual + .checked_sub(qmul(parameters.yield_maturity, regular_sum)) + .and_then(|value| { + value.checked_add(qmul(parameters.sqrt_maturity_over_sigma, singular_sum)) + }) + .ok_or(SolMathError::Overflow)?; + derivative = derivative + .checked_sub(qmul(parameters.yield_maturity, regular_derivative_sum)) + .and_then(|value| { + value.checked_add(qmul( + parameters.sqrt_maturity_over_sigma, + singular_derivative_sum, + )) + }) + .ok_or(SolMathError::Overflow)?; + Ok((residual, derivative)) +} + +#[inline(never)] +fn exercise_boundary(parameters: Parameters) -> Result { + let mut boundary = [0i64; KBI_NODES + 1]; + let mut log_boundary = [0i64; KBI_NODES + 1]; + let mut coefficient = [0i64; KBI_NODES + 1]; + let mut inverse_boundary = [0i64; KBI_NODES + 1]; + boundary[0] = expiry_boundary(parameters)?; + inverse_boundary[0] = qdiv(Q_ONE, boundary[0])?; + log_boundary[0] = qln(boundary[0])?; + coefficient[0] = parameters + .yield_rate + .checked_sub(qmul(parameters.rate, inverse_boundary[0])) + .ok_or(SolMathError::Overflow)?; + + // For lag=t_i*y_k^2 on the quadratic grid, each discount is + // exp(-qT*y_k^2*i^2/N^2). Advance it with two multiplications per node + // after evaluating only one exponential per Gaussian ordinate. + let mut boundary_discount = [Q_ONE; KBI_BOUNDARY_ORDER]; + let mut boundary_discount_ratio = [Q_ONE; KBI_BOUNDARY_ORDER]; + let mut boundary_discount_ratio_growth = [Q_ONE; KBI_BOUNDARY_ORDER]; + for point in 0..KBI_BOUNDARY_ORDER { + let base = exp_kernel(-qmul( + parameters.yield_maturity, + KBI_BOUNDARY_Y_SQUARED_OVER_NODES_SQUARED[point], + ))?; + boundary_discount_ratio[point] = base; + boundary_discount_ratio_growth[point] = qmul(base, base); + } + let nodes_squared = (KBI_NODES * KBI_NODES) as i64; + let node_discount_base = exp_kernel(-parameters.yield_maturity / nodes_squared)?; + let mut node_discount = Q_ONE; + let mut node_discount_ratio = node_discount_base; + let node_discount_ratio_growth = qmul(node_discount_base, node_discount_base); + + for index in 1..=KBI_NODES { + for point in 0..KBI_BOUNDARY_ORDER { + boundary_discount[point] = + qmul(boundary_discount[point], boundary_discount_ratio[point]); + boundary_discount_ratio[point] = qmul( + boundary_discount_ratio[point], + boundary_discount_ratio_growth[point], + ); + } + node_discount = qmul(node_discount, node_discount_ratio); + node_discount_ratio = qmul(node_discount_ratio, node_discount_ratio_growth); + let upper = boundary[index - 1].max(BOUNDARY_FLOOR_Q); + let mut candidate = if index == 1 { + let predictor_scale = if boundary[0] > BOUNDARY_HIGH_SWITCH_Q { + FIRST_NODE_HIGH_SCALE_Q + } else { + FIRST_NODE_LOW_SCALE_Q + }; + let sqrt_fraction = index as i64 * Q_ONE / KBI_NODES as i64; + let exponent = -qmul( + predictor_scale, + qmul(parameters.sigma_sqrt_maturity, sqrt_fraction), + ); + qmul(upper, qexp(exponent)?) + } else { + let extrapolated = qmul( + qmul(boundary[index - 1], boundary[index - 1]), + inverse_boundary[index - 2], + ); + if index == 3 { + boundary[index - 1] + + qmul( + extrapolated - boundary[index - 1], + THIRD_NODE_EXTRAPOLATION_Q, + ) + } else { + extrapolated + } + }; + candidate = candidate.clamp(BOUNDARY_FLOOR_Q, (upper - 1).max(BOUNDARY_FLOOR_Q)); + let steps = if index <= 2 { 2 } else { 1 }; + for _ in 0..steps { + let (residual, derivative) = boundary_residual( + index, + candidate, + &log_boundary, + &coefficient, + parameters, + node_discount, + &boundary_discount, + )?; + let mut proposal = if derivative.abs() < 1 { + candidate / 2 + } else { + candidate + .checked_sub(qdiv(residual, derivative)?) + .ok_or(SolMathError::Overflow)? + }; + proposal = proposal.clamp(BOUNDARY_FLOOR_Q, (upper - 1).max(BOUNDARY_FLOOR_Q)); + let low = qmul(candidate, STEP_LOW_Q); + let high = qmul(candidate, STEP_HIGH_Q); + candidate = proposal.clamp(low.max(BOUNDARY_FLOOR_Q), high.min(upper)); + } + boundary[index] = candidate.min(upper); + inverse_boundary[index] = qdiv(Q_ONE, boundary[index])?; + log_boundary[index] = qln(boundary[index])?; + coefficient[index] = parameters + .yield_rate + .checked_sub(qmul(parameters.rate, inverse_boundary[index])) + .ok_or(SolMathError::Overflow)?; + } + Ok(ExerciseBoundary { + values: boundary, + log_values: log_boundary, + }) +} + +#[inline(never)] +fn american_put_normalized( + normalized_spot: i64, + parameters: Parameters, +) -> Result { + let intrinsic = (Q_ONE - normalized_spot).max(0); + let european = european_put_normalized(normalized_spot, parameters)?; + // The positive early-exercise premium is bounded above by rT in these + // normalized units. This analytic gate avoids an ill-conditioned boundary + // only when its maximum dollar contribution is already negligible. + if parameters.rate_maturity <= NEGLIGIBLE_PREMIUM_Q { + return Ok(european.max(intrinsic)); + } + + let boundary = exercise_boundary(parameters)?; + if normalized_spot <= boundary.values[KBI_NODES] { + return Ok(intrinsic); + } + + let log_spot = qln(normalized_spot)?; + let mut rate_sum = 0i64; + let mut yield_sum = 0i64; + for point in 0..KBI_PRICE_POINTS { + let left = KBI_PRICE_BOUNDARY_LEFT[point] as usize; + let fraction = KBI_PRICE_BOUNDARY_FRACTION[point]; + let log_boundary_sample = boundary.log_values[left] + .checked_add(qmul( + boundary.log_values[left + 1] - boundary.log_values[left], + fraction, + )) + .ok_or(SolMathError::Overflow)?; + let lag_fraction = KBI_PRICE_LAG_FRACTION[point]; + let inverse_volatility_lag = qmul( + parameters.inverse_sigma_sqrt_maturity, + KBI_PRICE_INV_SQRT_LAG_FRACTION[point], + ); + let drift_lag = qmul(parameters.drift_maturity, lag_fraction); + let numerator = log_spot + .checked_sub(log_boundary_sample) + .and_then(|value| value.checked_add(drift_lag)) + .ok_or(SolMathError::Overflow)?; + let d1 = qmul(numerator, inverse_volatility_lag); + let volatility_lag = qmul( + parameters.sigma_sqrt_maturity, + KBI_PRICE_SQRT_LAG_FRACTION[point], + ); + let d2 = d1 + .checked_sub(volatility_lag) + .ok_or(SolMathError::Overflow)?; + let n_d1 = premium_cdf(-d1); + let n_d2 = premium_cdf(-d2); + let rate_discount = exp_kernel(-qmul(parameters.rate_maturity, lag_fraction))?; + let yield_discount = exp_kernel(-qmul(parameters.yield_maturity, lag_fraction))?; + let weight = KBI_PRICE_WEIGHT[point]; + rate_sum = rate_sum + .checked_add(qmul(weight, qmul(rate_discount, n_d2))) + .ok_or(SolMathError::Overflow)?; + yield_sum = yield_sum + .checked_add(qmul(weight, qmul(yield_discount, n_d1))) + .ok_or(SolMathError::Overflow)?; + } + let rate_premium = qmul(parameters.rate_maturity, rate_sum); + let yield_premium = qmul(qmul(parameters.yield_maturity, normalized_spot), yield_sum); + Ok(european + .checked_add(rate_premium) + .and_then(|value| value.checked_sub(yield_premium)) + .ok_or(SolMathError::Overflow)? + .clamp(intrinsic, Q_ONE)) +} + +#[inline(never)] +fn price_put_leg( + spot: u128, + strike: u128, + rate: u128, + dividend_yield: u128, + sigma: u128, + maturity: u128, +) -> Result { + if spot == 0 || strike == 0 || spot > MAX_SAFE_QUOTE || strike > MAX_SAFE_QUOTE { + return Err(SolMathError::DomainError); + } + let normalized_spot = spot + .checked_mul(Q_ONE as u128) + .ok_or(SolMathError::Overflow)? + / strike; + if normalized_spot < MIN_NORMALIZED_SPOT_Q as u128 + || normalized_spot > MAX_NORMALIZED_SPOT_Q as u128 + { + return Err(SolMathError::DomainError); + } + let normalized_price = american_put_normalized( + normalized_spot as i64, + parameters(rate, dividend_yield, sigma, maturity)?, + )?; + strike + .checked_mul(normalized_price as u128) + .ok_or(SolMathError::Overflow) + .map(|value| value / Q_ONE as u128) +} + +/// Price an American call or put from six scalar inputs, entirely on-chain. +#[allow(clippy::too_many_arguments)] +pub fn american_kbi_price( + spot: u128, + strike: u128, + rate: u128, + dividend_yield: u128, + sigma: u128, + maturity: u128, + kind: AmericanKbiKind, +) -> Result { + match kind { + AmericanKbiKind::Put => price_put_leg(spot, strike, rate, dividend_yield, sigma, maturity), + AmericanKbiKind::Call => price_put_leg(strike, spot, dividend_yield, rate, sigma, maturity), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generated_geometry_is_well_formed() { + assert_eq!(KBI_DATA_BITS, Q_BITS); + assert_eq!(KBI_NODES, 18); + assert_eq!(KBI_PRICE_ORDER, 9); + assert_eq!(KBI_PRICE_POINTS, 9); + assert_eq!(KBI_BOUNDARY_ORDER, 6); + assert_eq!(KBI_BOUNDARY_POINTS, 108); + assert_eq!(KBI_GRADING, 2 * Q_ONE); + assert_eq!(KBI_PRICE_POWER, 9 * Q_ONE / 4); + assert_eq!(KBI_TIME_FRACTION[0], 0); + assert_eq!(KBI_TIME_FRACTION[KBI_NODES], Q_ONE); + } + + #[test] + fn scale_bridge_is_round_trip_stable() { + for value in [-8 * SCALE_I, -SCALE_I, -1, 0, 1, SCALE_I, 8 * SCALE_I] { + assert!(q_to_scale(scale_to_q(value).unwrap()).abs_diff(value) <= 1); + } + } + + #[test] + fn boundary_normal_kernel_is_shape_preserving() { + let mut previous = 0i64; + for index in -512..=512i64 { + let x = index * Q_ONE / 64; + let (cdf, pdf) = boundary_cdf_and_pdf(x); + assert!( + cdf >= previous, + "index={index} x={x} previous={previous} cdf={cdf}" + ); + assert!((0..=Q_ONE).contains(&cdf)); + assert!((0..=PDF_ZERO_Q).contains(&pdf)); + let exact = crate::normal::norm_cdf_poly(q_to_scale(x)).unwrap(); + assert!(q_to_scale(cdf).abs_diff(exact) <= 5_500_000); + previous = cdf; + } + } + + #[test] + fn kernel_discount_error_is_bounded_on_the_option_domain() { + for index in 0..=240i64 { + let x = -index * Q_ONE / 1_000; + let exact = + scale_to_q(crate::transcendental::exp_fixed_i(q_to_scale(x)).unwrap()).unwrap(); + assert!(exp_kernel(x).unwrap().abs_diff(exact) <= 300_000); + } + } + + #[test] + fn reference_quote_tracks_qdfp() { + let call = american_kbi_price( + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + 30_000_000_000, + 300_000_000_000, + SCALE, + AmericanKbiKind::Call, + ) + .unwrap(); + let put = american_kbi_price( + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + 30_000_000_000, + 300_000_000_000, + SCALE, + AmericanKbiKind::Put, + ) + .unwrap(); + assert!(call.abs_diff(12_447_377_336_083) <= 20_000_000_000); + assert!(put.abs_diff(10_790_235_865_957) <= 20_000_000_000); + } + + #[test] + fn input_domain_fails_closed() { + assert_eq!( + american_kbi_price( + 100 * SCALE, + 100 * SCALE, + 0, + 0, + MIN_SIGMA - 1, + SCALE, + AmericanKbiKind::Put, + ), + Err(SolMathError::DomainError) + ); + assert_eq!( + american_kbi_price( + 100 * SCALE, + 100 * SCALE, + 0, + MAX_RATE + 1, + MIN_SIGMA, + SCALE, + AmericanKbiKind::Put, + ), + Err(SolMathError::DomainError) + ); + assert_eq!( + american_kbi_price( + 3 * 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + 30_000_000_000, + 300_000_000_000, + SCALE, + AmericanKbiKind::Put, + ), + Err(SolMathError::DomainError) + ); + } + + #[test] + fn boundary_regression_contract_zero() { + let parameters = parameters( + 92_840_699_462, + 47_682_052_097, + 363_772_111_820, + 1_942_465_753_425, + ) + .unwrap(); + let boundary = exercise_boundary(parameters).unwrap(); + let terminal = q_to_scale(boundary.values[KBI_NODES]); + assert!( + terminal.abs_diff(593_462_260_054) < 3_000_000, + "terminal={terminal}" + ); + let price = q_to_scale( + american_put_normalized(scale_to_q(597_127_273_422).unwrap(), parameters).unwrap(), + ); + assert!(price.abs_diff(402_893_802_428) < 3_000_000, "price={price}"); + } +} diff --git a/src/american_kbi_data.rs b/src/american_kbi_data.rs new file mode 100644 index 0000000..2569728 --- /dev/null +++ b/src/american_kbi_data.rs @@ -0,0 +1,1048 @@ +// @generated by scripts/generate_american_kbi_data.py; do not edit. +// SHA-256: 6c0e7857669b9913770de45da32d5cfdeb1d89faf78e7bcdad55d3ee27a218ca + +pub(crate) const KBI_NODES: usize = 18; +pub(crate) const KBI_PRICE_ORDER: usize = 9; +pub(crate) const KBI_PRICE_POINTS: usize = 9; +pub(crate) const KBI_BOUNDARY_ORDER: usize = 6; +pub(crate) const KBI_BOUNDARY_POINTS: usize = 108; +pub(crate) const KBI_DATA_BITS: u32 = 40; +pub(crate) const KBI_GRADING: i64 = 2199023255552; +pub(crate) const KBI_PRICE_POWER: i64 = 2473901162496; +pub(crate) const KBI_DATA_SHA256: [u8; 32] = [ + 0x6c, 0x0e, 0x78, 0x57, 0x66, 0x9b, 0x99, 0x13, 0x77, 0x0d, 0xe4, 0x5d, 0xa3, 0x2d, 0x5c, 0xfd, + 0xeb, 0x1d, 0x89, 0xfa, 0xf7, 0x8e, 0x7b, 0xcd, 0xad, 0x55, 0xd3, 0xee, 0x27, 0xa2, 0x18, 0xca, +]; + +pub(crate) const KBI_TIME_FRACTION: [i64; 19] = [ + 0, + 3393554407, + 13574217627, + 30541989660, + 54296870507, + 84838860168, + 122167958642, + 166284165929, + 217187482030, + 274877906944, + 339355440672, + 410620083213, + 488671834567, + 573510694735, + 665136663716, + 763549741511, + 868749928119, + 980737223541, + 1099511627776, +]; + +pub(crate) const KBI_BOUNDARY_Y_SQUARED_OVER_NODES_SQUARED: [i64; 6] = [ + 3868963, 97377263, 491811503, 1301578694, 2341227290, 3168254992, +]; + +pub(crate) const KBI_BOUNDARY_LAG_FRACTION: [i64; 108] = [ + 3868963, + 97377263, + 491811503, + 1301578694, + 2341227290, + 3168254992, + 15475852, + 389509052, + 1967246014, + 5206314776, + 9364909161, + 12673019968, + 34820667, + 876395367, + 4426303531, + 11714208245, + 21071045612, + 28514294928, + 61903407, + 1558036208, + 7868984055, + 20825259102, + 37459636644, + 50692079872, + 96724074, + 2434431576, + 12295287586, + 32539467347, + 58530682256, + 79206374801, + 139282667, + 3505581469, + 17705214123, + 46856832980, + 84284182448, + 114057179713, + 189579185, + 4771485888, + 24098763668, + 63777356000, + 114720137221, + 155244494609, + 247613630, + 6232144834, + 31475936219, + 83301036409, + 149838546575, + 202768319489, + 313386000, + 7887558305, + 39836731777, + 105427874205, + 189639410509, + 256628654354, + 386896297, + 9737726303, + 49181150343, + 130157869388, + 234122729023, + 316825499202, + 468144519, + 11782648827, + 59509191914, + 157491021960, + 283288502118, + 383358854035, + 557130667, + 14022325876, + 70820856493, + 187427331919, + 337136729794, + 456228718851, + 653854742, + 16456757452, + 83116144079, + 219966799267, + 395667412050, + 535435093651, + 758316742, + 19085943554, + 96395054671, + 255109424001, + 458880548886, + 620977978436, + 870516668, + 21909884182, + 110657588271, + 292855206124, + 526776140303, + 712857373205, + 990454520, + 24928579335, + 125903744877, + 333204145635, + 599354186300, + 811073277957, + 1118130298, + 28142029015, + 142133524490, + 376156242533, + 676614686878, + 915625692694, + 1253544002, + 31550233221, + 159346927110, + 421711496819, + 758557642036, + 1026514617415, +]; + +pub(crate) const KBI_BOUNDARY_INV_SQRT_LAG_FRACTION: [i64; 108] = [ + 586141475703460, + 116834460633587, + 51987675387183, + 31956891225870, + 23827471071624, + 20482816576932, + 293070737851730, + 58417230316793, + 25993837693592, + 15978445612935, + 11913735535812, + 10241408288466, + 195380491901153, + 38944820211196, + 17329225129061, + 10652297075290, + 7942490357208, + 6827605525644, + 146535368925865, + 29208615158397, + 12996918846796, + 7989222806467, + 5956867767906, + 5120704144233, + 117228295140692, + 23366892126717, + 10397535077437, + 6391378245174, + 4765494214325, + 4096563315386, + 97690245950577, + 19472410105598, + 8664612564531, + 5326148537645, + 3971245178604, + 3413802762822, + 83734496529066, + 16690637233370, + 7426810769598, + 4565270175124, + 3403924438804, + 2926116653847, + 73267684462932, + 14604307579198, + 6498459423398, + 3994611403234, + 2978433883953, + 2560352072116, + 65126830633718, + 12981606737065, + 5776408376354, + 3550765691763, + 2647496785736, + 2275868508548, + 58614147570346, + 11683446063359, + 5198767538718, + 3195689122587, + 2382747107162, + 2048281657693, + 53285588700315, + 10621314603053, + 4726152307926, + 2905171929625, + 2166133733784, + 1862074234267, + 48845122975288, + 9736205052799, + 4332306282265, + 2663074268822, + 1985622589302, + 1706901381411, + 45087805823343, + 8987266202584, + 3999051952860, + 2458222401990, + 1832882390125, + 1575601275149, + 41867248264533, + 8345318616685, + 3713405384799, + 2282635087562, + 1701962219402, + 1463058326924, + 39076098380231, + 7788964042239, + 3465845025812, + 2130459415058, + 1588498071442, + 1365521105129, + 36633842231466, + 7302153789599, + 3249229711699, + 1997305701617, + 1489216941977, + 1280176036058, + 34478910335498, + 6872615331387, + 3058098552187, + 1879817130934, + 1401615945390, + 1204871563349, + 32563415316859, + 6490803368533, + 2888204188177, + 1775382845882, + 1323748392868, + 1137934254274, +]; + +pub(crate) const KBI_BOUNDARY_REGULAR_WEIGHT: [i64; 108] = [ + 19631078, + 207384580, + 604495021, + 983396373, + 1016879446, + 561767908, + 78524312, + 829538321, + 2417980085, + 3933585494, + 4067517783, + 2247071632, + 176679702, + 1866461222, + 5440455191, + 8850567361, + 9151915012, + 5055911173, + 314097248, + 3318153284, + 9671920340, + 15734341974, + 16270071132, + 8988286530, + 490776950, + 5184614506, + 15112375531, + 24584909335, + 25421986144, + 14044197703, + 706718808, + 7465844889, + 21761820764, + 35402269443, + 36607660047, + 20223644692, + 961922821, + 10161844432, + 29620256040, + 48186422297, + 49827092841, + 27526627497, + 1256388991, + 13272613136, + 38687681359, + 62937367898, + 65080284528, + 35953146119, + 1590117317, + 16798151000, + 48964096720, + 79655106246, + 82367235105, + 45503200557, + 1963107799, + 20738458025, + 60449502123, + 98339637340, + 101687944574, + 56176790811, + 2375360436, + 25093534210, + 73143897569, + 118990961182, + 123042412935, + 67973916881, + 2826875230, + 29863379555, + 87047283057, + 141609077770, + 146430640187, + 80894578767, + 3317652180, + 35047994062, + 102159658588, + 166193987105, + 171852626331, + 94938776470, + 3847691285, + 40647377728, + 118481024161, + 192745689187, + 199308371366, + 110106509989, + 4416992547, + 46661530555, + 136011379776, + 221264184016, + 228797875293, + 126397779324, + 5025555964, + 53090452543, + 154750725435, + 251749471592, + 260321138111, + 143812584475, + 5673381538, + 59934143691, + 174699061135, + 284201551914, + 293878159820, + 162350925442, + 6360469268, + 67192604000, + 195856386878, + 318620424983, + 329468940421, + 182012802226, +]; + +pub(crate) const KBI_BOUNDARY_SINGULAR_WEIGHT: [i64; 108] = [ + 10465181750, + 22036752468, + 28582045103, + 28582045103, + 22036752468, + 10465181750, + 20930363499, + 44073504936, + 57164090207, + 57164090207, + 44073504936, + 20930363499, + 31395545249, + 66110257404, + 85746135310, + 85746135310, + 66110257404, + 31395545249, + 41860726999, + 88147009871, + 114328180414, + 114328180414, + 88147009871, + 41860726999, + 52325908748, + 110183762339, + 142910225517, + 142910225517, + 110183762339, + 52325908748, + 62791090498, + 132220514807, + 171492270620, + 171492270620, + 132220514807, + 62791090498, + 73256272248, + 154257267275, + 200074315724, + 200074315724, + 154257267275, + 73256272248, + 83721453997, + 176294019743, + 228656360827, + 228656360827, + 176294019743, + 83721453997, + 94186635747, + 198330772211, + 257238405931, + 257238405931, + 198330772211, + 94186635747, + 104651817497, + 220367524678, + 285820451034, + 285820451034, + 220367524678, + 104651817497, + 115116999246, + 242404277146, + 314402496137, + 314402496137, + 242404277146, + 115116999246, + 125582180996, + 264441029614, + 342984541241, + 342984541241, + 264441029614, + 125582180996, + 136047362745, + 286477782082, + 371566586344, + 371566586344, + 286477782082, + 136047362745, + 146512544495, + 308514534550, + 400148631448, + 400148631448, + 308514534550, + 146512544495, + 156977726245, + 330551287018, + 428730676551, + 428730676551, + 330551287018, + 156977726245, + 167442907994, + 352588039486, + 457312721654, + 457312721654, + 352588039486, + 167442907994, + 177908089744, + 374624791953, + 485894766758, + 485894766758, + 374624791953, + 177908089744, + 188373271494, + 396661544421, + 514476811861, + 514476811861, + 396661544421, + 188373271494, +]; + +pub(crate) const KBI_BOUNDARY_LEFT: [u8; 108] = [ + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 2, 2, 2, 2, 1, 0, 3, 3, 3, 3, 2, 1, 4, 4, 4, 3, 2, 1, 5, 5, + 5, 4, 3, 1, 6, 6, 6, 5, 3, 1, 7, 7, 7, 6, 4, 2, 8, 8, 8, 7, 5, 2, 9, 9, 9, 7, 5, 2, 10, 10, 10, + 8, 6, 2, 11, 11, 11, 9, 6, 3, 12, 12, 12, 10, 7, 3, 13, 13, 12, 10, 7, 3, 14, 14, 13, 11, 8, 3, + 15, 15, 14, 12, 8, 4, 16, 16, 15, 13, 9, 4, 17, 17, 16, 14, 10, 4, +]; + +pub(crate) const KBI_BOUNDARY_FRACTION: [i64; 108] = [ + 1098258083774, + 1067961394555, + 940164700666, + 677800130957, + 340953985740, + 72997010361, + 1097840235774, + 1057444650147, + 887049058296, + 537229632018, + 88101438395, + 291988041445, + 1097255248573, + 1042721207977, + 812687158978, + 340430933502, + 656358081295, + 656973093252, + 1096646384344, + 1027396808984, + 735290080096, + 135599635048, + 211443452148, + 22813512668, + 1096029561105, + 1011872091050, + 656881274693, + 1007056946278, + 825160626480, + 241804543752, + 1095409120134, + 996256319051, + 578012593598, + 756513185561, + 339819833809, + 509460248410, + 1094786731154, + 980591517941, + 498896287131, + 520401429319, + 973020093041, + 825780626642, + 1094163173369, + 964897299364, + 419631405441, + 292060752410, + 469874338106, + 54752430404, + 1093538859297, + 949184045956, + 340270386841, + 68382723101, + 11771104596, + 302942265632, + 1092914027767, + 933457768716, + 260843590356, + 926929555647, + 600691625420, + 580330905005, + 1092288826623, + 917722188738, + 181369809667, + 685004215775, + 128693359587, + 886918348522, + 1091663352287, + 901979732824, + 101861301523, + 449619842526, + 731919642050, + 87994977435, + 1091037670324, + 886232051199, + 22326400514, + 218907588294, + 249676921938, + 348698585868, + 1090411826875, + 870480305131, + 1037704277233, + 1090364899525, + 863394096271, + 630258482976, + 1089785855349, + 854725335540, + 952577502065, + 846266195848, + 373288389052, + 932674668758, + 1089159780536, + 838967766334, + 868202907809, + 607486365013, + 995028010108, + 121672067564, + 1088533621216, + 823208070170, + 784434911062, + 372843435278, + 498697896267, + 389327772222, + 1087907391875, + 807446611669, + 701163221370, + 141481496071, + 24663266773, + 673205034738, +]; + +pub(crate) const KBI_BOUNDARY_CANDIDATE_LOG_FACTOR: [i64; 108] = [ + 1253544002, + 31550233221, + 159346927110, + 421711496819, + 758557642036, + 1026514617415, + 1671392002, + 42066977629, + 212462569480, + 562281995758, + 1011410189381, + 1099511627776, + 2256379203, + 56790419799, + 286824468798, + 759080694274, + 1099511627776, + 1099511627776, + 2865243432, + 72114818792, + 364221547680, + 963911992728, + 1099511627776, + 1099511627776, + 3482066671, + 87639536726, + 442630353083, + 1099511627776, + 1099511627776, + 1099511627776, + 4102507642, + 103255308725, + 521499034178, + 1099511627776, + 1099511627776, + 1099511627776, + 4724896622, + 118920109835, + 600615340645, + 1099511627776, + 1099511627776, + 1099511627776, + 5348454407, + 134614328412, + 679880222335, + 1099511627776, + 1099511627776, + 1099511627776, + 5972768479, + 150327581820, + 759241240935, + 1099511627776, + 1099511627776, + 1099511627776, + 6597600009, + 166053859060, + 838668037420, + 1099511627776, + 1099511627776, + 1099511627776, + 7222801153, + 181789439038, + 918141818109, + 1099511627776, + 1099511627776, + 1099511627776, + 7848275489, + 197531894952, + 997650326253, + 1099511627776, + 1099511627776, + 1099511627776, + 8473957452, + 213279576577, + 1077185227262, + 1099511627776, + 1099511627776, + 1099511627776, + 9099800901, + 229031322645, + 1099511627776, + 1099511627776, + 1099511627776, + 1099511627776, + 9725772427, + 244786292236, + 1099511627776, + 1099511627776, + 1099511627776, + 1099511627776, + 10351847240, + 260543861442, + 1099511627776, + 1099511627776, + 1099511627776, + 1099511627776, + 10978006560, + 276303557606, + 1099511627776, + 1099511627776, + 1099511627776, + 1099511627776, + 11604235901, + 292065016107, + 1099511627776, + 1099511627776, + 1099511627776, + 1099511627776, +]; + +pub(crate) const KBI_BOUNDARY_CANDIDATE_COEFFICIENT_FACTOR: [i64; 108] = [ + 1098258083774, + 1067961394555, + 940164700666, + 677800130957, + 340953985740, + 72997010361, + 1097840235774, + 1057444650147, + 887049058296, + 537229632018, + 88101438395, + 0, + 1097255248573, + 1042721207977, + 812687158978, + 340430933502, + 0, + 0, + 1096646384344, + 1027396808984, + 735290080096, + 135599635048, + 0, + 0, + 1096029561105, + 1011872091050, + 656881274693, + 0, + 0, + 0, + 1095409120134, + 996256319051, + 578012593598, + 0, + 0, + 0, + 1094786731154, + 980591517941, + 498896287131, + 0, + 0, + 0, + 1094163173369, + 964897299364, + 419631405441, + 0, + 0, + 0, + 1093538859297, + 949184045956, + 340270386841, + 0, + 0, + 0, + 1092914027767, + 933457768716, + 260843590356, + 0, + 0, + 0, + 1092288826623, + 917722188738, + 181369809667, + 0, + 0, + 0, + 1091663352287, + 901979732824, + 101861301523, + 0, + 0, + 0, + 1091037670324, + 886232051199, + 22326400514, + 0, + 0, + 0, + 1090411826875, + 870480305131, + 0, + 0, + 0, + 0, + 1089785855349, + 854725335540, + 0, + 0, + 0, + 0, + 1089159780536, + 838967766334, + 0, + 0, + 0, + 0, + 1088533621216, + 823208070170, + 0, + 0, + 0, + 0, + 1087907391875, + 807446611669, + 0, + 0, + 0, + 0, +]; + +pub(crate) const KBI_PRICE_LAG_FRACTION: [i64; 9] = [ + 98983856, + 3954536877, + 27245432200, + 95696489876, + 231143846582, + 434827818873, + 678085804436, + 907010709850, + 1060518766035, +]; + +pub(crate) const KBI_PRICE_SQRT_LAG_FRACTION: [i64; 9] = [ + 10432348767, + 65939815580, + 173079951201, + 324375404980, + 504128304111, + 691446485949, + 863460031853, + 998633477306, + 1079839207813, +]; + +pub(crate) const KBI_PRICE_INV_SQRT_LAG_FRACTION: [i64; 9] = [ + 115882419830159, + 18333776171206, + 6984782530981, + 3726934289882, + 2398051864490, + 1748401133249, + 1400094706203, + 1210580104800, + 1119542438233, +]; + +pub(crate) const KBI_PRICE_WEIGHT: [i64; 9] = [ + 978624494, + 9783366968, + 41448869744, + 98943969971, + 171544782720, + 232245144455, + 246579048600, + 197750390593, + 100237430232, +]; + +pub(crate) const KBI_PRICE_BOUNDARY_LEFT: [u8; 9] = [17, 17, 17, 17, 15, 13, 11, 7, 3]; + +pub(crate) const KBI_PRICE_BOUNDARY_FRACTION: [i64; 9] = [ + 1098595320079, + 1062903914972, + 847296769694, + 213635550067, + 1095517576044, + 1094077370015, + 152219991360, + 566281843138, + 391154650588, +]; + +pub(crate) const KBI_NORMAL_HERMITE_A: [i64; 48] = [ + -141786139, -135213803, -122574266, -104823726, -83268699, -59432183, -34904252, -11195755, + 10387853, 28839592, 43503524, 54085500, 60626266, 63445413, 63067035, 60138512, 55352683, + 49381321, 42824839, 36180118, 29825722, 24021862, 18921343, 14587412, 11014732, 8150403, + 5912833, 4207221, 2937148, 2012402, 1353547, 893922, 579801, 369394, 231205, 142189, 85931, + 51039, 29796, 17099, 9646, 5350, 2918, 1565, 825, 428, 218, 109, +]; + +pub(crate) const KBI_NORMAL_HERMITE_B: [i64; 48] = [ + -666830, + -427307358, + -834081786, + -1202706851, + -1517791940, + -1767895006, + -1946173254, + -2050581925, + -2083631124, + -2051762043, + -1964443091, + -1833108617, + -1670066762, + -1487489954, + -1296575876, + -1106933683, + -926215716, + -759983991, + -611776939, + -483327125, + -374875119, + -285527038, + -213611392, + -157002294, + -113388412, + -80478474, + -56143383, + -38501521, + -25957605, + -17206828, + -11215644, + -7189007, + -4531748, + -2809588, + -1713268, + -1027630, + -606315, + -351909, + -200932, + -112869, + -62376, + -33915, + -18143, + -9550, + -4946, + -2521, + -1264, + -624, +]; + +pub(crate) const KBI_NORMAL_HERMITE_C: [i64; 48] = [ + 54830209514, + 54403517438, + 53143261314, + 51107374945, + 48387490064, + 45102100088, + 41388013527, + 37390954264, + 33256203149, + 29120104461, + 25103099148, + 21304723540, + 17800762805, + 14642508077, + 11857864408, + 9453913763, + 7420461935, + 5734088551, + 4362264532, + 3267185172, + 2409071276, + 1748798202, + 1249809714, + 879350960, + 609108608, + 415375981, + 278870242, + 184321974, + 119940596, + 76836831, + 48460382, + 30089735, + 18393486, + 11069395, + 6558399, + 3825481, + 2196789, + 1241952, + 691250, + 378774, + 204333, + 108521, + 56741, + 29208, + 14802, + 7385, + 3627, + 1754, +]; + +pub(crate) const KBI_NORMAL_HERMITE_D: [i64; 48] = [ + 549755813888, + 604443570434, + 658284566711, + 710471171974, + 760271016341, + 807057445767, + 850332218666, + 889739154687, + 925068331271, + 956251291149, + 983348473157, + 1006530632739, + 1026056333162, + 1042247655470, + 1055466119005, + 1066090474573, + 1074497593166, + 1081047192067, + 1086070677948, + 1089863990381, + 1092684028546, + 1094748050424, + 1096235343451, + 1097290463116, + 1098027399194, + 1098534134122, + 1098877182032, + 1099105821724, + 1099255849399, + 1099352769538, + 1099414411943, + 1099453010228, + 1099476804877, + 1099491246417, + 1099499875618, + 1099504951955, + 1099507891995, + 1099509568400, + 1099510509481, + 1099511029595, + 1099511312599, + 1099511464203, + 1099511544159, + 1099511585674, + 1099511606897, + 1099511617578, + 1099511622870, + 1099511625451, +]; diff --git a/src/arithmetic.rs b/src/arithmetic.rs index a45d44d..09df264 100644 --- a/src/arithmetic.rs +++ b/src/arithmetic.rs @@ -1,7 +1,75 @@ use crate::constants::*; use crate::double_word::DoubleWord; use crate::error::SolMathError; -use crate::overflow::{checked_mul_div_i, checked_mul_div_u, checked_mul_div_rem_u}; +use crate::overflow::{checked_mul_div_i, checked_mul_div_rem_u, checked_mul_div_u}; + +/// Return whether a proper unsigned remainder is at least one half of its +/// divisor without evaluating `2 * remainder` (which may overflow). +/// +/// Callers must maintain `divisor > 0` and `remainder < divisor`. +#[inline] +fn remainder_at_least_half(remainder: u128, divisor: u128) -> bool { + debug_assert!(divisor > 0); + debug_assert!(remainder < divisor); + remainder >= divisor - remainder +} + +/// Round an exact unsigned quotient/remainder pair to nearest, with ties up. +/// +/// Callers must maintain `divisor > 0` and `remainder < divisor`. +#[inline] +fn round_unsigned_quotient(quotient: u128, remainder: u128, divisor: u128) -> Option { + debug_assert!(divisor > 0); + debug_assert!(remainder < divisor); + if remainder_at_least_half(remainder, divisor) { + quotient.checked_add(1) + } else { + Some(quotient) + } +} + +/// Round an exact signed truncating quotient/remainder pair to nearest, with +/// ties away from zero. +/// +/// Callers must maintain `divisor > 0` and `|remainder| < divisor`. +#[inline] +fn round_signed_quotient( + quotient: i128, + remainder: i128, + divisor: u128, +) -> Result { + debug_assert!(divisor > 0); + debug_assert!(remainder.unsigned_abs() < divisor); + if !remainder_at_least_half(remainder.unsigned_abs(), divisor) { + Ok(quotient) + } else if remainder >= 0 { + quotient.checked_add(1).ok_or(SolMathError::Overflow) + } else { + quotient.checked_sub(1).ok_or(SolMathError::Overflow) + } +} + +/// Clamp a computed European call to hard bounds and derive its put from +/// put-call parity. Shared by standard and HP pricing paths. +#[cfg(feature = "transcendental")] +pub(crate) fn european_prices_from_call( + call_i: i128, + s: u128, + k_disc_i: i128, +) -> Result<(u128, u128), SolMathError> { + if k_disc_i < 0 || s > i128::MAX as u128 { + return Err(SolMathError::Overflow); + } + let k_disc = k_disc_i as u128; + let call = if call_i <= 0 { 0 } else { call_i as u128 }.clamp(s.saturating_sub(k_disc), s); + let put = if call >= s { + (call - s).checked_add(k_disc) + } else { + k_disc.checked_sub(s - call) + } + .ok_or(SolMathError::Overflow)?; + Ok((call, put)) +} /// Unsigned fixed-point multiply: `(a * b) / SCALE`. /// @@ -49,9 +117,17 @@ pub fn fp_mul_i(a: i128, b: i128) -> Result { /// Internal unchecked fixed-point multiply. Inputs MUST be bounded /// such that |a * b| < i128::MAX. No overflow check — caller's /// responsibility. Not exposed in public API. +/// +/// The `debug_assert` enforces the precondition in debug/test builds at zero +/// release cost, so any caller that violates the bound is caught by the test +/// and fuzz suites rather than silently wrapping. #[allow(dead_code)] #[inline] pub(crate) fn fp_mul_i_fast(a: i128, b: i128) -> i128 { + debug_assert!( + a.checked_mul(b).is_some(), + "fp_mul_i_fast precondition violated: {a} * {b} overflows i128" + ); a * b / SCALE_I } @@ -61,34 +137,29 @@ pub(crate) fn fp_mul_i_fast(a: i128, b: i128) -> i128 { pub fn fp_mul_i_round(a: i128, b: i128) -> Result { match a.checked_mul(b) { Some(p) => { - if p >= 0 { - match p.checked_add(SCALE_I / 2) { - Some(v) => Ok(v / SCALE_I), - None => Ok(p / SCALE_I + 1), - } - } else { - match p.checked_sub(SCALE_I / 2) { - Some(v) => Ok(v / SCALE_I), - None => Ok(p / SCALE_I - 1), - } - } + let quotient = p / SCALE_I; + let remainder = p % SCALE_I; + round_signed_quotient(quotient, remainder, SCALE) } None => { // Overflow path: use U256 intermediate with correct rounding. let neg = (a < 0) != (b < 0); let (q, rem) = checked_mul_div_rem_u(a.unsigned_abs(), b.unsigned_abs(), SCALE) .ok_or(SolMathError::Overflow)?; - let rounded = if rem >= SCALE / 2 { - q.checked_add(1).ok_or(SolMathError::Overflow)? - } else { - q - }; + let rounded = round_unsigned_quotient(q, rem, SCALE).ok_or(SolMathError::Overflow)?; if neg { - if rounded == (1u128 << 127) { Ok(i128::MIN) } - else if rounded < (1u128 << 127) { Ok(-(rounded as i128)) } - else { Err(SolMathError::Overflow) } - } else if rounded <= i128::MAX as u128 { Ok(rounded as i128) } - else { Err(SolMathError::Overflow) } + if rounded == (1u128 << 127) { + Ok(i128::MIN) + } else if rounded < (1u128 << 127) { + Ok(-(rounded as i128)) + } else { + Err(SolMathError::Overflow) + } + } else if rounded <= i128::MAX as u128 { + Ok(rounded as i128) + } else { + Err(SolMathError::Overflow) + } } } } @@ -167,7 +238,11 @@ pub fn fp_div_ceil(a: u128, b: u128) -> Result { } match fp_div_rem_experimental_u(a, b) { Some((q, rem)) => { - if rem > 0 { Ok(q.checked_add(1).ok_or(SolMathError::Overflow)?) } else { Ok(q) } + if rem > 0 { + Ok(q.checked_add(1).ok_or(SolMathError::Overflow)?) + } else { + Ok(q) + } } None => Err(SolMathError::Overflow), } @@ -179,20 +254,35 @@ pub fn fp_div_ceil(a: u128, b: u128) -> Result { #[allow(dead_code)] #[inline] pub(crate) fn fp_div_i_round(a: i128, b: i128) -> Result { - if b == 0 { return Err(SolMathError::DivisionByZero); } - if a == 0 { return Ok(0); } + if b == 0 { + return Err(SolMathError::DivisionByZero); + } + if a == 0 { + return Ok(0); + } let neg = (a < 0) ^ (b < 0); let (q, r) = fp_div_rem_experimental_u(a.unsigned_abs(), b.unsigned_abs()) .ok_or(SolMathError::Overflow)?; - let round_up = r >= b.unsigned_abs() / 2; - let q = if round_up { q.checked_add(1).ok_or(SolMathError::Overflow)? } else { q }; + let divisor = b.unsigned_abs(); + // Compare r/divisor with 1/2 without computing 2*r (which could + // overflow). `divisor - r` is the exact ceiling-half threshold for odd + // divisors; using `divisor / 2` would incorrectly round a remainder of + // floor(divisor/2) up when the divisor is odd. + let q = round_unsigned_quotient(q, r, divisor).ok_or(SolMathError::Overflow)?; if neg { - if q == (1u128 << 127) { Ok(i128::MIN) } - else if q < (1u128 << 127) { Ok(-(q as i128)) } - else { Err(SolMathError::Overflow) } - } else if q <= i128::MAX as u128 { Ok(q as i128) } - else { Err(SolMathError::Overflow) } + if q == (1u128 << 127) { + Ok(i128::MIN) + } else if q < (1u128 << 127) { + Ok(-(q as i128)) + } else { + Err(SolMathError::Overflow) + } + } else if q <= i128::MAX as u128 { + Ok(q as i128) + } else { + Err(SolMathError::Overflow) + } } /// Fractional tail of fixed-point division. Internal — called by fp_div_rem_experimental_u. @@ -250,20 +340,51 @@ pub(crate) fn fp_div_rem_experimental_u(a: u128, b: u128) -> Option<(u128, u128) Some((base.checked_add(frac)?, rem)) } -/// Integer square root via Newton's method. Internal — used by fp_sqrt. +/// Return whether `candidate` is the exact floor of `sqrt(radicand)`. +#[inline] +fn floor_sqrt_certificate(candidate: u128, radicand: u128) -> bool { + candidate + .checked_mul(candidate) + .is_some_and(|square| square <= radicand) + && candidate.checked_add(1).is_some_and(|next| { + next.checked_mul(next) + .map_or(true, |square| square > radicand) + }) +} + +/// Apply one binary-search decision to an integer-square-root bracket. +#[inline] +fn bisect_sqrt_bracket(low: u128, high: u128, midpoint_is_feasible: bool) -> (u128, u128) { + let midpoint = low + (high - low) / 2; + if midpoint_is_feasible { + (midpoint, high) + } else { + (low, midpoint) + } +} + +/// Average a Newton candidate with its exact truncating quotient. +#[inline] +fn average_sqrt_newton_candidate(candidate: u128, quotient: u128) -> u128 { + debug_assert!(candidate.checked_add(quotient).is_some()); + (candidate + quotient) / 2 +} + +/// Exact integer square root via monotonically decreasing Newton iteration. #[allow(dead_code)] #[inline] pub(crate) fn isqrt_u128(n: u128) -> u128 { if n == 0 { return 0; } - let mut x = 1u128 << ((128 - n.leading_zeros() + 1) / 2); + let mut candidate = 1u128 << ((128 - n.leading_zeros() + 1) / 2); loop { - let x1 = (x + n / x) / 2; - if x1 >= x { - return x; + let next = average_sqrt_newton_candidate(candidate, n / candidate); + if next >= candidate { + debug_assert!(floor_sqrt_certificate(candidate, n)); + return candidate; } - x = x1; + candidate = next; } } @@ -275,40 +396,32 @@ pub(crate) fn sqrt_scaled_newton(scaled: u128) -> u128 { if guess == 0 { break; } - let new_guess = (guess + scaled / guess) / 2; + let new_guess = average_sqrt_newton_candidate(guess, scaled / guess); if new_guess == guess || new_guess + 1 == guess { guess = guess.min(new_guess); break; } guess = new_guess; } - debug_assert!( - guess.checked_mul(guess).map_or(false, |sq| sq <= scaled) - && (guess + 1).checked_mul(guess + 1).map_or(true, |sq| sq > scaled), - "sqrt_scaled_newton: post-check failed for scaled={}, guess={}", - scaled, guess - ); - guess + // Newton preserves `guess >= floor(sqrt(scaled))`; one feasible square is + // therefore a complete exactness certificate. + if guess + .checked_mul(guess) + .is_some_and(|square| square <= scaled) + { + guess + } else { + // Restart through the monotonically convergent exact integer kernel. + // Correctness does not depend on the fixed iteration count above. + isqrt_u128(scaled) + } } /// Widening 128×128 → 256-bit multiply. Internal — used by fp_sqrt overflow path. pub(crate) fn wide_mul_u128(a: u128, b: u128) -> (u128, u128) { - let mask = u128::from(u64::MAX); - let a0 = a & mask; - let a1 = a >> 64; - let b0 = b & mask; - let b1 = b >> 64; - - let p0 = a0 * b0; - let p1 = a0 * b1; - let p2 = a1 * b0; - let p3 = a1 * b1; - - let carry0 = p0 >> 64; - let mid = (p1 & mask) + (p2 & mask) + carry0; - let lo = (p0 & mask) | ((mid & mask) << 64); - let hi = p3 + (p1 >> 64) + (p2 >> 64) + (mid >> 64); - (hi, lo) + let product = U256::mul_u128(a, b); + let high = u128::from(product.limbs[2]) | (u128::from(product.limbs[3]) << 64); + (high, product.low_u128()) } /// Compare two 256-bit (hi, lo) pairs. Internal — used by fp_sqrt bisection. @@ -351,14 +464,17 @@ pub fn fp_sqrt(x: u128) -> Result { let mut scale_back = 1u128; while reduced > u128::MAX / SCALE { // sqrt(4a) = 2*sqrt(a); round while reducing to preserve monotonicity. - reduced = (reduced + 2) / 4; + reduced = reduced / 4 + (reduced % 4 + 2) / 4; scale_back = scale_back.checked_mul(2).ok_or(SolMathError::Overflow)?; } let approx = sqrt_scaled_newton(reduced * SCALE) - .checked_mul(scale_back).ok_or(SolMathError::Overflow)?; + .checked_mul(scale_back) + .ok_or(SolMathError::Overflow)?; - let mut low = approx.checked_sub(scale_back).ok_or(SolMathError::Overflow)?; + let mut low = approx + .checked_sub(scale_back) + .ok_or(SolMathError::Overflow)?; while cmp_sqrt_candidate(low, x).is_gt() { if low == 0 { break; @@ -366,7 +482,9 @@ pub fn fp_sqrt(x: u128) -> Result { low = low.checked_sub(scale_back).ok_or(SolMathError::Overflow)?; } - let mut high = approx.checked_add(scale_back).ok_or(SolMathError::Overflow)?; + let mut high = approx + .checked_add(scale_back) + .ok_or(SolMathError::Overflow)?; while !cmp_sqrt_candidate(high, x).is_gt() { low = high; high = high.checked_add(scale_back).ok_or(SolMathError::Overflow)?; @@ -374,11 +492,7 @@ pub fn fp_sqrt(x: u128) -> Result { while low + 1 < high { let mid = low + (high - low) / 2; - if cmp_sqrt_candidate(mid, x).is_gt() { - high = mid; - } else { - low = mid; - } + (low, high) = bisect_sqrt_bracket(low, high, !cmp_sqrt_candidate(mid, x).is_gt()); } Ok(low) @@ -389,9 +503,7 @@ pub fn fp_sqrt(x: u128) -> Result { #[inline] pub fn fp_mul_round(a: u128, b: u128) -> Result { match checked_mul_div_rem_u(a, b, SCALE) { - Some((q, r)) => { - if r >= SCALE / 2 { q.checked_add(1).ok_or(SolMathError::Overflow) } else { Ok(q) } - } + Some((q, r)) => round_unsigned_quotient(q, r, SCALE).ok_or(SolMathError::Overflow), None => Err(SolMathError::Overflow), } } @@ -415,23 +527,26 @@ pub fn fp_mul_i_round_dw(a: i128, b: i128) -> Result { let aa = a.unsigned_abs(); let bb = b.unsigned_abs(); - let (q_trunc, r_trunc) = checked_mul_div_rem_u(aa, bb, SCALE) - .ok_or(SolMathError::Overflow)?; + let (q_trunc, r_trunc) = checked_mul_div_rem_u(aa, bb, SCALE).ok_or(SolMathError::Overflow)?; // q_trunc = floor(|a*b| / SCALE), r_trunc in [0, SCALE) let half = SCALE / 2; let (q, lo) = if r_trunc >= half { - (q_trunc + 1, r_trunc as i128 - SCALE_I) // lo in (-SCALE/2, 0] + ( + q_trunc.checked_add(1).ok_or(SolMathError::Overflow)?, + r_trunc as i128 - SCALE_I, + ) // lo in (-SCALE/2, 0] } else { - (q_trunc, r_trunc as i128) // lo in [0, SCALE/2) + (q_trunc, r_trunc as i128) // lo in [0, SCALE/2) }; if neg { if q == (1u128 << 127) { - if lo != 0 { + let signed_lo = -lo; + if signed_lo < 0 { return Err(SolMathError::Overflow); } - Ok(DoubleWord::new_raw(i128::MIN, 0)) + Ok(DoubleWord::new_raw(i128::MIN, signed_lo)) } else if q < (1u128 << 127) { Ok(DoubleWord::new_raw(-(q as i128), -lo)) } else { @@ -452,11 +567,188 @@ pub fn fp_div_round(a: u128, b: u128) -> Result { return Err(SolMathError::DivisionByZero); } let (q, r) = fp_div_rem_experimental_u(a, b).ok_or(SolMathError::Overflow)?; - Ok(if r >= b / 2 { - q.checked_add(1).ok_or(SolMathError::Overflow)? - } else { - q - }) + // `r >= b - r` is equivalent to `2*r >= b` without overflow and is + // correct for odd divisors. `r >= b/2` rounds values just below one half + // upward when b is odd. + round_unsigned_quotient(q, r, b).ok_or(SolMathError::Overflow) +} + +#[cfg(kani)] +mod verification { + use super::*; + + /// Prove the production Newton averaging transition preserves the floor + /// lower bound and cannot satisfy the exit condition above the floor. The + /// assumptions are the exact quotient inequalities derived from + /// `floor_root² <= n < (floor_root+1)²` and Rust's `/` semantics. + #[kani::proof] + fn sqrt_newton_transition_preserves_and_detects_the_floor() { + let candidate: u128 = kani::any(); + let quotient: u128 = kani::any(); + let floor_root: u128 = kani::any(); + kani::assume(floor_root <= u64::MAX as u128); + kani::assume(candidate >= floor_root); + kani::assume(candidate.checked_add(quotient).is_some()); + kani::assume(candidate + quotient >= 2 * floor_root); + kani::assume(candidate == floor_root || quotient < candidate); + + let next = average_sqrt_newton_candidate(candidate, quotient); + + assert!(next >= floor_root); + if next >= candidate { + assert_eq!(candidate, floor_root); + } + } + + /// Prove one production bisection transition preserves a bracket around + /// the mathematical floor root and strictly shrinks it. Exact integer + /// multiplication makes `midpoint² <= n` equivalent to + /// `midpoint <= floor(sqrt(n))`; induction from `[0, 2^64)` therefore + /// proves the fallback's exact floor postcondition. + #[kani::proof] + fn sqrt_bisection_preserves_the_exact_floor_bracket() { + const LIMIT: u128 = 1u128 << 64; + + let low: u128 = kani::any(); + let high: u128 = kani::any(); + let floor_root: u128 = kani::any(); + kani::assume(low <= floor_root); + kani::assume(floor_root < high); + kani::assume(high <= LIMIT); + kani::assume(low + 1 < high); + + let previous_width = high - low; + let midpoint = low + previous_width / 2; + let (next_low, next_high) = bisect_sqrt_bracket(low, high, midpoint <= floor_root); + + assert!(next_low <= floor_root); + assert!(floor_root < next_high); + assert!(next_low < next_high); + assert!(next_high - next_low < previous_width); + } + + /// Prove truncating division by the standard fixed-point scale produces + /// an exact quotient/remainder decomposition whose residual is strictly + /// smaller than one output ULP. + #[kani::proof] + fn signed_scale_remainder_is_sub_ulp() { + let product: i128 = kani::any(); + let remainder = product % SCALE_I; + + assert!(remainder.unsigned_abs() < SCALE); + } + + /// Prove the production signed quotient/remainder rounding primitive is + /// nearest with ties away from zero. Successful results have at most + /// one-half ULP error; errors occur exactly when the required one-unit + /// correction is outside `i128`. + #[kani::proof] + fn signed_quotient_rounding_is_half_ulp_or_overflow() { + let quotient: i128 = kani::any(); + let remainder: i128 = kani::any(); + let divisor: u128 = kani::any(); + kani::assume(divisor > 0); + kani::assume(remainder.unsigned_abs() < divisor); + + let remainder_magnitude = remainder.unsigned_abs(); + let round_away = remainder_at_least_half(remainder_magnitude, divisor); + let expected = if !round_away { + Some(quotient) + } else if remainder >= 0 { + quotient.checked_add(1) + } else { + quotient.checked_sub(1) + }; + let actual = round_signed_quotient(quotient, remainder, divisor); + + if let Some(expected) = expected { + assert_eq!(actual, Ok(expected)); + let error_numerator = if round_away { + divisor - remainder_magnitude + } else { + remainder_magnitude + }; + assert!(error_numerator <= divisor - error_numerator); + } else { + assert_eq!(actual, Err(SolMathError::Overflow)); + } + } + + /// Prove the production unsigned quotient/remainder rounding primitive is + /// nearest with ties up. Successful results have at most one-half ULP + /// error; overflow is reported exactly for `MAX + 1`. + #[kani::proof] + fn unsigned_quotient_rounding_is_half_ulp_or_overflow() { + let quotient: u128 = kani::any(); + let remainder: u128 = kani::any(); + let divisor: u128 = kani::any(); + kani::assume(divisor > 0); + kani::assume(remainder < divisor); + + let round_up = remainder_at_least_half(remainder, divisor); + let expected = if round_up { + quotient.checked_add(1) + } else { + Some(quotient) + }; + let actual = round_unsigned_quotient(quotient, remainder, divisor); + + assert_eq!(actual, expected); + if expected.is_some() { + let error_numerator = if round_up { + divisor - remainder + } else { + remainder + }; + // `e <= divisor - e` is the overflow-free form of `2*e <= divisor`. + assert!(error_numerator <= divisor - error_numerator); + } + } + + /// Bit-precise proof over every `(remainder, divisor)` pair satisfying the + /// division invariant. This covers the cases where `2 * remainder` would + /// overflow as well as odd divisors and exact ties. + #[kani::proof] + fn remainder_half_comparison_is_exact_and_overflow_free() { + let remainder: u128 = kani::any(); + let divisor: u128 = kani::any(); + kani::assume(divisor > 0); + kani::assume(remainder < divisor); + + let actual = remainder_at_least_half(remainder, divisor); + match remainder.checked_mul(2) { + Some(twice) => assert_eq!(actual, twice >= divisor), + None => { + // If doubling overflows u128 then remainder > MAX/2 while + // divisor <= MAX, so the exact rational remainder is > 1/2. + assert!(actual); + } + } + } + + /// Prove the shared European-price constructor enforces hard call/put + /// bounds and exact put-call parity for its complete accepted domain. + #[cfg(feature = "transcendental")] + #[kani::proof] + fn european_prices_preserve_bounds_and_parity() { + let call_i: i128 = kani::any(); + let spot: u128 = kani::any(); + let discounted_strike_i: i128 = kani::any(); + kani::assume(spot <= i128::MAX as u128); + kani::assume(discounted_strike_i >= 0); + + let discounted_strike = discounted_strike_i as u128; + let (call, put) = european_prices_from_call(call_i, spot, discounted_strike_i) + .expect("the bounded domain must be representable"); + + assert!(call <= spot); + assert!(call >= spot.saturating_sub(discounted_strike)); + assert!(put <= discounted_strike); + assert_eq!( + call.checked_add(discounted_strike).unwrap(), + put.checked_add(spot).unwrap(), + ); + } } #[cfg(test)] @@ -478,15 +770,24 @@ mod tests { #[test] fn test_fp_mul_round_vs_truncating() { let cases: &[(u128, u128)] = &[ - (SCALE / 3, SCALE), (SCALE, SCALE / 3), - (SCALE / 7, SCALE / 11), (2 * SCALE, SCALE / 3), - (SCALE + 1, SCALE + 1), (SCALE * 1000, SCALE / 997), + (SCALE / 3, SCALE), + (SCALE, SCALE / 3), + (SCALE / 7, SCALE / 11), + (2 * SCALE, SCALE / 3), + (SCALE + 1, SCALE + 1), + (SCALE * 1000, SCALE / 997), ]; for &(a, b) in cases { let trunc = fp_mul(a, b).unwrap(); let round = fp_mul_round(a, b).unwrap(); - assert!(round == trunc || round == trunc + 1, - "a={}, b={}: trunc={}, round={}", a, b, trunc, round); + assert!( + round == trunc || round == trunc + 1, + "a={}, b={}: trunc={}, round={}", + a, + b, + trunc, + round + ); } } @@ -500,7 +801,19 @@ mod tests { #[test] fn test_fp_mul_round_overflow_is_error() { - assert!(matches!(fp_mul_round(u128::MAX, u128::MAX), Err(SolMathError::Overflow))); + assert!(matches!( + fp_mul_round(u128::MAX, u128::MAX), + Err(SolMathError::Overflow) + )); + } + + #[test] + fn test_fp_mul_i_round_near_extrema_uses_the_actual_remainder() { + let positive = i128::MAX - 300_000_000_000; + let negative = i128::MIN + 300_000_000_000; + let expected = 170_141_183_460_469_231_731_687_303i128; + assert_eq!(fp_mul_i_round(positive, 1).unwrap(), expected); + assert_eq!(fp_mul_i_round(negative, 1).unwrap(), -expected); } // ===== fp_mul_i_round_dw tests ===== @@ -508,25 +821,46 @@ mod tests { #[test] fn test_dw_mul_quotient_matches_fp_mul_i_round() { let values: &[i128] = &[ - 0, 1, -1, 2, -2, - SCALE_I / 2, -SCALE_I / 2, - SCALE_I, -SCALE_I, - SCALE_I + 1, -(SCALE_I + 1), - SCALE_I - 1, -(SCALE_I - 1), - SCALE_I * 2, -(SCALE_I * 2), - SCALE_I * 50, -(SCALE_I * 50), - SCALE_I / 3, -(SCALE_I / 3), - SCALE_I / 7, -(SCALE_I / 7), - 999_999_999_999, -999_999_999_999, - 500_000_000_001, -500_000_000_001, + 0, + 1, + -1, + 2, + -2, + SCALE_I / 2, + -SCALE_I / 2, + SCALE_I, + -SCALE_I, + SCALE_I + 1, + -(SCALE_I + 1), + SCALE_I - 1, + -(SCALE_I - 1), + SCALE_I * 2, + -(SCALE_I * 2), + SCALE_I * 50, + -(SCALE_I * 50), + SCALE_I / 3, + -(SCALE_I / 3), + SCALE_I / 7, + -(SCALE_I / 7), + 999_999_999_999, + -999_999_999_999, + 500_000_000_001, + -500_000_000_001, ]; for &a in values { for &b in values { if a.checked_mul(b).is_some() { let expected = fp_mul_i_round(a, b).unwrap(); let dw = fp_mul_i_round_dw(a, b).unwrap(); - assert_eq!(dw.hi(), expected, - "Quotient mismatch: a={}, b={}, expected={}, got={}", a, b, expected, dw.hi()); + assert_eq!( + dw.hi(), + expected, + "Quotient mismatch: a={}, b={}, expected={}, got={}", + a, + b, + expected, + dw.hi() + ); } } } @@ -535,14 +869,27 @@ mod tests { #[test] fn test_dw_mul_remainder_bounded() { let values: &[i128] = &[ - 0, 1, -1, SCALE_I, -SCALE_I, SCALE_I / 3, -SCALE_I / 7, - SCALE_I * 50, -SCALE_I * 50, 999_999_999_999, + 0, + 1, + -1, + SCALE_I, + -SCALE_I, + SCALE_I / 3, + -SCALE_I / 7, + SCALE_I * 50, + -SCALE_I * 50, + 999_999_999_999, ]; for &a in values { for &b in values { let dw = fp_mul_i_round_dw(a, b).unwrap(); - assert!(dw.lo().abs() < SCALE_I, - "Remainder out of bounds: a={}, b={}, lo={}", a, b, dw.lo()); + assert!( + dw.lo().abs() < SCALE_I, + "Remainder out of bounds: a={}, b={}, lo={}", + a, + b, + dw.lo() + ); } } } @@ -550,7 +897,11 @@ mod tests { #[test] fn test_dw_mul_remainder_sign_convention() { let dw = fp_mul_i_round_dw(SCALE_I / 3, SCALE_I).unwrap(); - assert!(dw.lo() >= 0, "Expected non-negative lo for 1/3: lo={}", dw.lo()); + assert!( + dw.lo() >= 0, + "Expected non-negative lo for 1/3: lo={}", + dw.lo() + ); let dw2 = fp_mul_i_round_dw(2 * SCALE_I / 3, SCALE_I).unwrap(); assert!(dw2.lo().abs() < SCALE_I); @@ -581,18 +932,31 @@ mod tests { assert_eq!(dw.lo(), 0); } + #[test] + fn test_dw_mul_allows_rounded_i128_min_with_positive_residual() { + let a = -1_000_000_000_001i128; + let b = 170_141_183_460_299_090_548_227_004_625_335_878_723i128; + let dw = fp_mul_i_round_dw(a, b).unwrap(); + assert_eq!(dw.hi(), i128::MIN); + assert_eq!(dw.lo(), 374_664_121_277); + assert_eq!(fp_mul_i_round_dw(-a, b), Err(SolMathError::Overflow)); + } + #[test] fn test_dw_mul_to_i128_matches_fp_mul_i_round() { let values: &[i128] = &[ - SCALE_I / 3, SCALE_I / 7, SCALE_I * 2, -SCALE_I / 3, -SCALE_I * 5, + SCALE_I / 3, + SCALE_I / 7, + SCALE_I * 2, + -SCALE_I / 3, + -SCALE_I * 5, ]; for &a in values { for &b in values { if a.checked_mul(b).is_some() { let direct = fp_mul_i_round(a, b).unwrap(); let via_dw = fp_mul_i_round_dw(a, b).unwrap().to_i128(); - assert_eq!(direct, via_dw, - "Roundtrip mismatch: a={}, b={}", a, b); + assert_eq!(direct, via_dw, "Roundtrip mismatch: a={}, b={}", a, b); } } } @@ -614,6 +978,16 @@ mod tests { assert_eq!(r, t + 1, "2/3 should round up"); } + #[test] + fn test_fp_div_round_odd_divisor_below_half_does_not_round_up() { + // SCALE / 3 = 333_333_333_333 + 1/3. The old `r >= b / 2` + // comparison treated 1/3 as at least one half because integer b/2 is + // 1 for b=3. + assert_eq!(fp_div_round(1, 3).unwrap(), SCALE / 3); + assert_eq!(fp_div_i_round(1, 3).unwrap(), SCALE_I / 3); + assert_eq!(fp_div_i_round(-1, 3).unwrap(), -(SCALE_I / 3)); + } + #[test] fn test_fp_div_round_no_round() { let t = fp_div(SCALE, 3 * SCALE).unwrap(); @@ -624,15 +998,20 @@ mod tests { #[test] fn test_fp_div_round_agrees_with_signed() { let cases: &[(u128, u128)] = &[ - (SCALE, 3 * SCALE), (2 * SCALE, 3 * SCALE), - (7 * SCALE, 11 * SCALE), (SCALE, 7 * SCALE), + (SCALE, 3 * SCALE), + (2 * SCALE, 3 * SCALE), + (7 * SCALE, 11 * SCALE), + (SCALE, 7 * SCALE), (100 * SCALE, 97 * SCALE), ]; for &(a, b) in cases { let unsigned_r = fp_div_round(a, b).unwrap(); let signed_r = fp_div_i_round(a as i128, b as i128).unwrap(); - assert_eq!(unsigned_r as i128, signed_r, - "Disagree for a={}, b={}", a, b); + assert_eq!( + unsigned_r as i128, signed_r, + "Disagree for a={}, b={}", + a, b + ); } } @@ -643,15 +1022,24 @@ mod tests { for &b in values { let t = fp_div(a, b).unwrap(); let r = fp_div_round(a, b).unwrap(); - assert!(r == t || r == t + 1, - "a={}, b={}: trunc={}, round={}", a, b, t, r); + assert!( + r == t || r == t + 1, + "a={}, b={}: trunc={}, round={}", + a, + b, + t, + r + ); } } } #[test] fn test_fp_div_round_zero_divisor() { - assert!(matches!(fp_div_round(SCALE, 0), Err(SolMathError::DivisionByZero))); + assert!(matches!( + fp_div_round(SCALE, 0), + Err(SolMathError::DivisionByZero) + )); } #[test] @@ -661,4 +1049,12 @@ mod tests { assert!(fp_div_round(SCALE, large).is_ok()); } + #[test] + fn test_fp_sqrt_maximum_input() { + assert_eq!( + fp_sqrt(u128::MAX).unwrap(), + 18_446_744_073_709_551_615_999_999 + ); + assert!(fp_sqrt(u128::MAX - 1).unwrap() <= fp_sqrt(u128::MAX).unwrap()); + } } diff --git a/src/asian.rs b/src/asian.rs new file mode 100644 index 0000000..a3da2eb --- /dev/null +++ b/src/asian.rs @@ -0,0 +1,682 @@ +//! Continuous arithmetic-Asian / TWAP-settled option pricing. +//! +//! The final settlement value is modelled as +//! +//! ```text +//! A = w A_fixed + (1 - w) / tau * integral_[T-tau,T] S(u) du +//! ``` +//! +//! where `T` is time to payment, `tau` is the remaining averaging-window +//! length, and `w` is the fraction of the final average already observed. +//! This covers both common TWAP states: +//! +//! - before averaging starts: `w = 0`, `tau < T`; +//! - while averaging is running: `w > 0`, normally `tau = T`. +//! +//! Arithmetic averages of lognormal prices are not themselves lognormal. The +//! pricer therefore computes the first two risk-neutral moments of the +//! remaining **continuous arithmetic average exactly**, then prices a +//! moment-matched lognormal distribution (the Levy/Turnbull-Wakeman family of +//! approximations). The approximation is constant-time, scalar-only, `no_std`, +//! and suitable for an on-chain mark; it is not an exact arithmetic-Asian +//! distribution and does not model discrete oracle sampling error. +//! +//! All public values use [`crate::SCALE`] (`1e12`). Internal moment and option +//! calculations use `1e15` fixed point to retain the small variance of short +//! crypto settlement windows. + +use crate::arithmetic::{european_prices_from_call, isqrt_u128}; +use crate::constants::{SCALE, SCALE_HP, SCALE_HP_U}; +use crate::error::SolMathError; +use crate::hp::{ + downscale_hp_to_std, downscale_hp_to_std_i, exp_fixed_hp, fp_div_hp_safe, fp_mul_hp_i, + upscale_std_to_hp, +}; +use crate::normal::norm_cdf_poly; +use crate::transcendental::{ln_1p_fixed, ln_fixed_i}; + +const MAX_PRICE: u128 = 100_000 * SCALE; +const MAX_RATE: u128 = 10 * SCALE; +const MAX_VOL: u128 = 100 * SCALE; +const MAX_TIME: u128 = 100 * SCALE; +const HP_TO_STD: i128 = 1_000; +// Below 1e-4 dimensionless carry over the averaging window, the closed form's +// O(B) numerator loses material digits before division by B. A second-order +// expansion has O(B^3) truncation instead and is stable at HP precision. +const SMALL_WINDOW_CARRY_HP: u128 = 100_000_000_000; + +/// Use the certified SCALE logarithm for final option transforms. Moment +/// construction remains at HP precision; a SCALE log contributes at most a +/// few 1e-12 units, below the public price scale, while avoiding the +/// compensated HP polynomial's on-chain cost. +#[inline] +fn ln_hp_via_std(x_hp: i128) -> Result { + let x_std = downscale_hp_to_std_i(x_hp); + if x_std <= 0 { + return Err(SolMathError::DomainError); + } + ln_fixed_i(x_std as u128)? + .checked_mul(HP_TO_STD) + .ok_or(SolMathError::Overflow) +} + +/// `ln(1+x)` at HP interface precision via the cancellation-safe standard +/// kernel. `x` is first rounded once to the public 1e12 scale. +#[inline] +fn ln_1p_hp_via_std(x_hp: i128) -> Result { + ln_1p_fixed(downscale_hp_to_std_i(x_hp))? + .checked_mul(HP_TO_STD) + .ok_or(SolMathError::Overflow) +} + +/// Standard CDF lifted to HP scale for final price multiplication. The CDF +/// kernel is certified within two SCALE ULP and is roughly 20-35x cheaper on +/// SBF than the research HP polynomial. +#[inline] +fn cdf_hp_via_std(x_hp: i128) -> Result { + norm_cdf_poly(downscale_hp_to_std_i(x_hp))? + .checked_mul(HP_TO_STD) + .ok_or(SolMathError::Overflow) +} + +/// Prices and diagnostics for a moment-matched arithmetic-Asian option. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AsianOptionResult { + /// Discounted call price at `SCALE`. + pub call: u128, + /// Discounted put price at `SCALE`. + pub put: u128, + /// Risk-neutral expected final arithmetic average at `SCALE`. + pub expected_average: u128, + /// Log-variance of the matched lognormal distribution at `SCALE`. + /// Zero denotes a deterministic settlement value at fixed-point precision. + pub log_variance: u128, +} + +#[derive(Debug, Clone, Copy)] +struct FutureAverageMoments { + mean_hp: i128, + variance_hp: i128, +} + +/// `exp(x) - 1` at HP precision, with a series that preserves very small TWAP +/// exponents instead of subtracting two nearly equal `1e15` values. +#[inline] +fn expm1_hp(x: i128) -> Result { + if x.unsigned_abs() < 10_000_000_000_000 { + let x2 = fp_mul_hp_i(x, x)?; + let x3 = fp_mul_hp_i(x2, x)?; + let x4 = fp_mul_hp_i(x3, x)?; + let x5 = fp_mul_hp_i(x4, x)?; + let x6 = fp_mul_hp_i(x5, x)?; + return x + .checked_add(x2 / 2) + .and_then(|v| v.checked_add(x3 / 6)) + .and_then(|v| v.checked_add(x4 / 24)) + .and_then(|v| v.checked_add(x5 / 120)) + .and_then(|v| v.checked_add(x6 / 720)) + .ok_or(SolMathError::Overflow); + } + exp_fixed_hp(x)? + .checked_sub(SCALE_HP) + .ok_or(SolMathError::Overflow) +} + +/// `expm1(x) / x`, continuously extended to one at `x = 0`. +#[inline] +fn phi1_hp(x: i128) -> Result { + if x == 0 { + return Ok(SCALE_HP); + } + // Dividing expm1's tiny x^2 correction by x magnifies its fixed-point + // rounding. Evaluate phi1's own series directly in short-window regimes. + if x.unsigned_abs() < 10_000_000_000_000 { + let x2 = fp_mul_hp_i(x, x)?; + let x3 = fp_mul_hp_i(x2, x)?; + let x4 = fp_mul_hp_i(x3, x)?; + let x5 = fp_mul_hp_i(x4, x)?; + return SCALE_HP + .checked_add(x / 2) + .and_then(|v| v.checked_add(x2 / 6)) + .and_then(|v| v.checked_add(x3 / 24)) + .and_then(|v| v.checked_add(x4 / 120)) + .and_then(|v| v.checked_add(x5 / 720)) + .ok_or(SolMathError::Overflow); + } + fp_div_hp_safe(expm1_hp(x)?, x) +} + +/// Dimensionless second moment of an arithmetic average over a unit interval. +/// +/// With `B = carry * tau` and `V = sigma^2 * tau`, this evaluates +/// +/// ```text +/// J(B,V) = 2 int_0^1 exp((B+V)u) int_u^1 exp(Bv) dv du. +/// ``` +/// +/// A short bivariate series avoids cancellation in the settlement-window +/// regime where both `B` and `V` are tiny. Outside that regime, the closed form +/// uses three exponentials regardless of maturity. +fn average_second_factor_hp(b_window: i128, variance_window: i128) -> Result { + let a = b_window + .checked_add(variance_window) + .ok_or(SolMathError::Overflow)?; + let series_size = a + .unsigned_abs() + .checked_add(b_window.unsigned_abs()) + .ok_or(SolMathError::Overflow)?; + + // Exact series: + // 2 sum_{m,n>=0} A^m B^n / [m! n! (m+1)(m+n+2)]. + // Total degree 8 has absolute remainder below 1.4e-11 when + // |A|+|B| <= 0.25; ordinary short TWAP windows are many orders smaller. + if series_size <= (SCALE_HP_U / 4) { + const DEGREE: usize = 8; + let mut sum = 0i128; + let mut a_term = SCALE_HP; + for m in 0..=DEGREE { + let mut b_term = SCALE_HP; + for n in 0..=(DEGREE - m) { + let product = fp_mul_hp_i(a_term, b_term)?; + let denominator = ((m + 1) * (m + n + 2)) as i128; + let term = product.checked_mul(2).ok_or(SolMathError::Overflow)? / denominator; + sum = sum.checked_add(term).ok_or(SolMathError::Overflow)?; + + if n < DEGREE - m { + b_term = fp_mul_hp_i(b_term, b_window)? / (n as i128 + 1); + } + } + if m < DEGREE { + a_term = fp_mul_hp_i(a_term, a)? / (m as i128 + 1); + } + } + return Ok(sum); + } + + if b_window.unsigned_abs() <= SMALL_WINDOW_CARRY_HP { + if variance_window == 0 { + return Ok(SCALE_HP); + } + let expm1_v = expm1_hp(variance_window)?; + // J(0,V) = 2 * (expm1(V) - V) / V^2. + let numerator = expm1_v + .checked_sub(variance_window) + .and_then(|v| v.checked_mul(2)) + .ok_or(SolMathError::Overflow)?; + let v2 = fp_mul_hp_i(variance_window, variance_window)?; + let j0 = fp_div_hp_safe(numerator, v2)?; + if b_window == 0 { + return Ok(j0); + } + + // J(B,V) = J(0,V) + B J1(V) + B^2 J2(V) + O(B^3), where + // J_B(0,V) = integral_0^1 exp(Vu) * (1 + 2u - 3u^2) du. + // This branch is reached only for V > 0.25 (smaller V used the + // bivariate series above), so the following closed moments do not + // suffer origin cancellation. + let exp_v = SCALE_HP + .checked_add(expm1_v) + .ok_or(SolMathError::Overflow)?; + let i0 = fp_div_hp_safe(expm1_v, variance_window)?; + let i1_numerator = fp_mul_hp_i(variance_window - SCALE_HP, exp_v)? + .checked_add(SCALE_HP) + .ok_or(SolMathError::Overflow)?; + let i1 = fp_div_hp_safe(i1_numerator, v2)?; + let v3 = fp_mul_hp_i(v2, variance_window)?; + let i2_polynomial = v2 + .checked_sub(2 * variance_window) + .and_then(|value| value.checked_add(2 * SCALE_HP)) + .ok_or(SolMathError::Overflow)?; + let i2_numerator = fp_mul_hp_i(exp_v, i2_polynomial)? + .checked_sub(2 * SCALE_HP) + .ok_or(SolMathError::Overflow)?; + let i2 = fp_div_hp_safe(i2_numerator, v3)?; + let j1 = i0 + .checked_add(2 * i1) + .and_then(|value| value.checked_sub(3 * i2)) + .ok_or(SolMathError::Overflow)?; + let v4 = fp_mul_hp_i(v3, variance_window)?; + let i3_polynomial = v3 + .checked_sub(3 * v2) + .and_then(|value| value.checked_add(6 * variance_window)) + .and_then(|value| value.checked_sub(6 * SCALE_HP)) + .ok_or(SolMathError::Overflow)?; + let i3_numerator = fp_mul_hp_i(exp_v, i3_polynomial)? + .checked_add(6 * SCALE_HP) + .ok_or(SolMathError::Overflow)?; + let i3 = fp_div_hp_safe(i3_numerator, v4)?; + // J2(V) = integral exp(Vu) * (1 + 3u + 3u^2 - 7u^3) / 3 du. + let j2 = i0 + .checked_add(3 * i1) + .and_then(|value| value.checked_add(3 * i2)) + .and_then(|value| value.checked_sub(7 * i3)) + .ok_or(SolMathError::Overflow)? + / 3; + let b2 = fp_mul_hp_i(b_window, b_window)?; + let correction = fp_mul_hp_i(b_window, j1)? + .checked_add(fp_mul_hp_i(b2, j2)?) + .ok_or(SolMathError::Overflow)?; + return j0 + .checked_add(correction) + .filter(|value| *value > 0) + .ok_or(SolMathError::DomainError); + } + + // 2/B * [exp(B) phi1(B+V) - phi1(2B+V)]. + let first = fp_mul_hp_i(exp_fixed_hp(b_window)?, phi1_hp(a)?)?; + let two_b_plus_v = b_window + .checked_mul(2) + .and_then(|v| v.checked_add(variance_window)) + .ok_or(SolMathError::Overflow)?; + let bracket = first + .checked_sub(phi1_hp(two_b_plus_v)?) + .ok_or(SolMathError::Overflow)?; + fp_div_hp_safe( + bracket.checked_mul(2).ok_or(SolMathError::Overflow)?, + b_window, + ) +} + +fn future_average_moments_hp( + spot_hp: i128, + carry_hp: i128, + sigma_hp: i128, + time_hp: i128, + averaging_time_hp: i128, +) -> Result { + let start_hp = time_hp + .checked_sub(averaging_time_hp) + .ok_or(SolMathError::DomainError)?; + let sigma_sq_hp = fp_mul_hp_i(sigma_hp, sigma_hp)?; + let b_window = fp_mul_hp_i(carry_hp, averaging_time_hp)?; + let variance_window = fp_mul_hp_i(sigma_sq_hp, averaging_time_hp)?; + + // E[B] / S0 = exp(b * start) * phi1(b * tau). + let mean_start = exp_fixed_hp(fp_mul_hp_i(carry_hp, start_hp)?)?; + let mean_factor = fp_mul_hp_i(mean_start, phi1_hp(b_window)?)?; + + // E[B^2] / S0^2 = exp((2b + sigma^2) * start) * J(B,V). + let second_rate = carry_hp + .checked_mul(2) + .and_then(|v| v.checked_add(sigma_sq_hp)) + .ok_or(SolMathError::Overflow)?; + let second_start = exp_fixed_hp(fp_mul_hp_i(second_rate, start_hp)?)?; + let second_factor = fp_mul_hp_i( + second_start, + average_second_factor_hp(b_window, variance_window)?, + )?; + let mean_factor_sq = fp_mul_hp_i(mean_factor, mean_factor)?; + + let variance_factor = if second_factor >= mean_factor_sq { + second_factor - mean_factor_sq + } else { + // Rounding may make a genuinely deterministic/tiny-variance result a + // few HP units negative. Material violations fail closed. + let deficit = mean_factor_sq - second_factor; + let tolerance = (mean_factor_sq / SCALE_HP).max(16); + if deficit > tolerance { + return Err(SolMathError::DomainError); + } + 0 + }; + + let mean_hp = fp_mul_hp_i(spot_hp, mean_factor)?; + let spot_sq_hp = fp_mul_hp_i(spot_hp, spot_hp)?; + let variance_hp = fp_mul_hp_i(spot_sq_hp, variance_factor)?; + Ok(FutureAverageMoments { + mean_hp, + variance_hp, + }) +} + +fn price_matched_lognormal_hp( + mean_hp: i128, + variance_hp: i128, + strike_hp: i128, + rate_hp: i128, + time_hp: i128, +) -> Result { + if mean_hp <= 0 || variance_hp < 0 || strike_hp <= 0 { + return Err(SolMathError::DomainError); + } + + let discount_hp = exp_fixed_hp(-fp_mul_hp_i(rate_hp, time_hp)?)?; + let discounted_mean_hp = fp_mul_hp_i(mean_hp, discount_hp)?; + let discounted_strike_hp = fp_mul_hp_i(strike_hp, discount_hp)?; + + let mean_sq_hp = fp_mul_hp_i(mean_hp, mean_hp)?; + let cv_sq_hp = if variance_hp == 0 { + 0 + } else { + fp_div_hp_safe(variance_hp, mean_sq_hp)? + }; + let log_variance_hp = if cv_sq_hp == 0 { + 0 + } else { + ln_1p_hp_via_std(cv_sq_hp)? + }; + + let call_hp = if log_variance_hp <= 0 { + discounted_mean_hp + .checked_sub(discounted_strike_hp) + .unwrap_or(i128::MIN) + .max(0) + } else { + let radicand = (log_variance_hp as u128) + .checked_mul(SCALE_HP_U) + .ok_or(SolMathError::Overflow)?; + let sqrt_log_variance_hp = isqrt_u128(radicand) as i128; + if sqrt_log_variance_hp == 0 { + discounted_mean_hp + .checked_sub(discounted_strike_hp) + .unwrap_or(i128::MIN) + .max(0) + } else { + let mean_strike_ratio = fp_div_hp_safe(mean_hp, strike_hp)?; + let d1 = fp_div_hp_safe( + ln_hp_via_std(mean_strike_ratio)? + log_variance_hp / 2, + sqrt_log_variance_hp, + )?; + let d2 = d1 - sqrt_log_variance_hp; + let undiscounted_call = fp_mul_hp_i(mean_hp, cdf_hp_via_std(d1)?)? + .checked_sub(fp_mul_hp_i(strike_hp, cdf_hp_via_std(d2)?)?) + .ok_or(SolMathError::Overflow)?; + fp_mul_hp_i(discount_hp, undiscounted_call.max(0))? + } + }; + + let discounted_mean = downscale_hp_to_std(discounted_mean_hp); + let discounted_strike = downscale_hp_to_std_i(discounted_strike_hp); + let call_std = downscale_hp_to_std_i(call_hp); + let (call, put) = european_prices_from_call(call_std, discounted_mean, discounted_strike)?; + + Ok(AsianOptionResult { + call, + put, + expected_average: downscale_hp_to_std(mean_hp), + log_variance: downscale_hp_to_std(log_variance_hp), + }) +} + +fn validate_inputs( + s: u128, + k: u128, + r: u128, + q: u128, + sigma: u128, + t: u128, + averaging_time: u128, + fixed_average: u128, + fixed_weight: u128, +) -> Result<(), SolMathError> { + if s > MAX_PRICE || k > MAX_PRICE || fixed_average > MAX_PRICE { + return Err(SolMathError::Overflow); + } + if r > MAX_RATE || q > MAX_RATE || sigma > MAX_VOL || t > MAX_TIME { + return Err(SolMathError::Overflow); + } + if s == 0 || k == 0 || sigma == 0 || fixed_weight > SCALE { + return Err(SolMathError::DomainError); + } + if fixed_weight == 0 { + if fixed_average != 0 { + return Err(SolMathError::DomainError); + } + } else if fixed_average == 0 { + return Err(SolMathError::DomainError); + } + if fixed_weight < SCALE { + if t == 0 || averaging_time == 0 || averaging_time > t { + return Err(SolMathError::DomainError); + } + } else if averaging_time != 0 { + return Err(SolMathError::DomainError); + } + Ok(()) +} + +/// Price a continuously sampled, partially fixed arithmetic-Asian option. +/// +/// The remaining arithmetic average is sampled over the interval +/// `[t - averaging_time, t]`. `fixed_weight` is in `[0, SCALE]`; the future +/// average receives weight `SCALE - fixed_weight`. +/// +/// # Typical TWAP states +/// +/// - **Before a 30-minute window:** set `averaging_time = 30 minutes`, +/// `fixed_weight = 0`, and `fixed_average = 0`. +/// - **12 minutes into that window:** set `t = averaging_time = 18 minutes`, +/// `fixed_weight = 0.4`, and `fixed_average` to the observed 12-minute TWAP. +/// - **Fully fixed:** set `fixed_weight = 1` and `averaging_time = 0`; the +/// function returns discounted intrinsic value. +/// +/// Rates, times, prices, weights, and results are fixed point at `SCALE`. +/// `q` is the continuous yield, so risk-neutral carry is `r - q`. +/// +/// # Approximation +/// +/// The first two continuous-average moments under GBM are exact. Option prices +/// use a two-moment lognormal approximation, not an exact arithmetic-Asian law. +#[allow(clippy::too_many_arguments)] +pub fn arithmetic_asian_price( + s: u128, + k: u128, + r: u128, + q: u128, + sigma: u128, + t: u128, + averaging_time: u128, + fixed_average: u128, + fixed_weight: u128, +) -> Result { + validate_inputs( + s, + k, + r, + q, + sigma, + t, + averaging_time, + fixed_average, + fixed_weight, + )?; + + let strike_hp = upscale_std_to_hp(k)?; + let rate_hp = upscale_std_to_hp(r)?; + let time_hp = upscale_std_to_hp(t)?; + + if fixed_weight == SCALE { + return price_matched_lognormal_hp( + upscale_std_to_hp(fixed_average)?, + 0, + strike_hp, + rate_hp, + time_hp, + ); + } + + let spot_hp = upscale_std_to_hp(s)?; + let yield_hp = upscale_std_to_hp(q)?; + let sigma_hp = upscale_std_to_hp(sigma)?; + let averaging_time_hp = upscale_std_to_hp(averaging_time)?; + let carry_hp = rate_hp + .checked_sub(yield_hp) + .ok_or(SolMathError::Overflow)?; + let future = + future_average_moments_hp(spot_hp, carry_hp, sigma_hp, time_hp, averaging_time_hp)?; + + let fixed_weight_hp = upscale_std_to_hp(fixed_weight)?; + let future_weight_hp = SCALE_HP + .checked_sub(fixed_weight_hp) + .ok_or(SolMathError::DomainError)?; + let fixed_average_hp = upscale_std_to_hp(fixed_average)?; + let mean_hp = fp_mul_hp_i(fixed_weight_hp, fixed_average_hp)? + .checked_add(fp_mul_hp_i(future_weight_hp, future.mean_hp)?) + .ok_or(SolMathError::Overflow)?; + let future_weight_sq_hp = fp_mul_hp_i(future_weight_hp, future_weight_hp)?; + let variance_hp = fp_mul_hp_i(future_weight_sq_hp, future.variance_hp)?; + + price_matched_lognormal_hp(mean_hp, variance_hp, strike_hp, rate_hp, time_hp) +} + +/// TWAP-named alias of [`arithmetic_asian_price`]. +/// +/// This spelling is intended for protocols whose contract specification calls +/// the settlement average a TWAP rather than an arithmetic-Asian underlying. +#[allow(clippy::too_many_arguments)] +#[inline] +pub fn twap_option_price( + s: u128, + k: u128, + r: u128, + q: u128, + sigma: u128, + t: u128, + averaging_time: u128, + fixed_average: u128, + fixed_weight: u128, +) -> Result { + arithmetic_asian_price( + s, + k, + r, + q, + sigma, + t, + averaging_time, + fixed_average, + fixed_weight, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + const S: u128 = 100 * SCALE; + const K: u128 = 100 * SCALE; + const R: u128 = 5 * SCALE / 100; + const Q: u128 = 2 * SCALE / 100; + const SIGMA: u128 = 40 * SCALE / 100; + + #[test] + fn twap_alias_is_exact() { + let a = arithmetic_asian_price(S, K, R, Q, SIGMA, SCALE, SCALE / 2, 0, 0); + let b = twap_option_price(S, K, R, Q, SIGMA, SCALE, SCALE / 2, 0, 0); + assert_eq!(a, b); + } + + #[test] + fn fully_fixed_is_discounted_intrinsic() { + let result = + arithmetic_asian_price(S, K, R, Q, SIGMA, SCALE / 2, 0, 110 * SCALE, SCALE).unwrap(); + let discount = exp_fixed_hp( + -fp_mul_hp_i( + upscale_std_to_hp(R).unwrap(), + upscale_std_to_hp(SCALE / 2).unwrap(), + ) + .unwrap(), + ) + .unwrap(); + let discounted_average = + downscale_hp_to_std(fp_mul_hp_i(110 * SCALE_HP, discount).unwrap()); + let discounted_strike = downscale_hp_to_std(fp_mul_hp_i(100 * SCALE_HP, discount).unwrap()); + let expected = discounted_average - discounted_strike; + assert_eq!(result.call, expected); + assert_eq!(result.put, 0); + assert_eq!(result.expected_average, 110 * SCALE); + assert_eq!(result.log_variance, 0); + } + + #[test] + fn output_satisfies_exact_discounted_average_parity() { + let result = arithmetic_asian_price( + S, + 105 * SCALE, + R, + Q, + SIGMA, + SCALE, + SCALE / 12, + 98 * SCALE, + SCALE / 3, + ) + .unwrap(); + let discount = exp_fixed_hp( + -fp_mul_hp_i( + upscale_std_to_hp(R).unwrap(), + upscale_std_to_hp(SCALE).unwrap(), + ) + .unwrap(), + ) + .unwrap(); + let mean_disc = downscale_hp_to_std( + fp_mul_hp_i( + upscale_std_to_hp(result.expected_average).unwrap(), + discount, + ) + .unwrap(), + ); + let strike_disc = downscale_hp_to_std( + fp_mul_hp_i(upscale_std_to_hp(105 * SCALE).unwrap(), discount).unwrap(), + ); + assert_eq!( + result.call as i128 - result.put as i128, + mean_disc as i128 - strike_disc as i128 + ); + } + + #[test] + fn fixing_more_of_the_average_reduces_variance() { + let unseasoned = + arithmetic_asian_price(S, K, R, Q, SIGMA, SCALE / 365, SCALE / 365, 0, 0).unwrap(); + let seasoned = + arithmetic_asian_price(S, K, R, Q, SIGMA, SCALE / 730, SCALE / 730, S, SCALE / 2) + .unwrap(); + assert!(seasoned.log_variance < unseasoned.log_variance); + } + + #[test] + fn one_raw_carry_above_series_seam_is_stable() { + // Retained adversarial vector: B is only 7.8e-14 while V is above the + // bivariate-series seam. Direct division by B previously moved the + // price by about $9 because the closed-form numerator had lost its + // significant digits. + let result = arithmetic_asian_price( + 931_700_559_528_446, + 1_717_008_561_609_268, + 120_633_804_209, + 120_633_804_208, + 1_871_600_373_667, + 1_795_710_902_611, + 78_071_066_562, + 1_286_913_254_987_408, + 914_756_059_943, + ) + .unwrap(); + assert!(result.call.abs_diff(298_725_289_603_127) <= 5_000); + assert!(result.put.abs_diff(669_434_522_411_088) <= 5_000); + assert_eq!(result.expected_average, 1_256_633_525_268_358); + assert!(result.log_variance.abs_diff(1_027_764_862_497) <= 2); + } + + #[test] + fn invalid_average_state_fails_closed() { + assert_eq!( + arithmetic_asian_price(S, K, R, Q, SIGMA, SCALE, SCALE + 1, 0, 0), + Err(SolMathError::DomainError) + ); + assert_eq!( + arithmetic_asian_price(S, K, R, Q, SIGMA, SCALE, SCALE, S, 0), + Err(SolMathError::DomainError) + ); + assert_eq!( + arithmetic_asian_price(S, K, R, Q, SIGMA, SCALE, 0, S, SCALE / 2), + Err(SolMathError::DomainError) + ); + assert_eq!( + arithmetic_asian_price(S, K, R, Q, SIGMA, SCALE, 1, S, SCALE + 1), + Err(SolMathError::DomainError) + ); + } +} diff --git a/src/barrier.rs b/src/barrier.rs index afba279..e7125c7 100644 --- a/src/barrier.rs +++ b/src/barrier.rs @@ -3,12 +3,16 @@ // Uses Haug building blocks A, B, C, D with eta = phi (not barrier direction). // Verified against QuantLib AnalyticBarrierEngine on 443K vectors. // -// All arithmetic at HP precision (1e15). Single barriers: ~160K CU. +// All arithmetic at HP precision (1e15). Final SBF audit: 270,156 CU average, +// 415,531 max for the legacy/unbreached calculation. +use crate::arithmetic::{fp_div_i, fp_mul_i, isqrt_u128}; use crate::constants::*; use crate::error::SolMathError; -use crate::arithmetic::{fp_mul_i, fp_div_i, isqrt_u128}; -use crate::hp::{black_scholes_price_hp, fp_mul_hp_i, fp_div_hp_safe, upscale_std_to_hp, downscale_hp_to_std, ln_fixed_hp, exp_fixed_hp, norm_cdf_poly_hp}; +use crate::hp::{ + black_scholes_price_hp, downscale_hp_to_std, exp_fixed_hp, fp_div_hp_safe, fp_mul_hp_i, + ln_fixed_hp, norm_cdf_poly_hp, upscale_std_to_hp, +}; /// Barrier option type (single barrier, European exercise). #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -54,8 +58,12 @@ struct HaugIntermediates { /// eta = phi (call/put sign), NOT barrier direction. #[inline(never)] fn compute_intermediates( - s: u128, k: u128, h: u128, - r: u128, sigma: u128, t: u128, + s: u128, + k: u128, + h: u128, + r: u128, + sigma: u128, + t: u128, is_call: bool, ) -> Result { let s_hp = upscale_std_to_hp(s)?; @@ -65,7 +73,11 @@ fn compute_intermediates( let sigma_hp = upscale_std_to_hp(sigma)?; let t_hp = upscale_std_to_hp(t)?; - let sqrt_t_hp = isqrt_u128((t_hp as u128).checked_mul(SCALE_HP_U).ok_or(SolMathError::Overflow)?) as i128; + let sqrt_t_hp = isqrt_u128( + (t_hp as u128) + .checked_mul(SCALE_HP_U) + .ok_or(SolMathError::Overflow)?, + ) as i128; let sigma_sqrt_t_hp = fp_mul_hp_i(sigma_hp, sqrt_t_hp)?; let r_t_hp = fp_mul_hp_i(r_hp, t_hp)?; @@ -73,7 +85,10 @@ fn compute_intermediates( let k_disc_hp = fp_mul_hp_i(k_hp, discount_hp)?; let sigma_sq_hp = fp_mul_hp_i(sigma_hp, sigma_hp)?; - let drift_hp = fp_mul_hp_i(r_hp + sigma_sq_hp / 2, t_hp)?; + let drift_rate_hp = r_hp + .checked_add(sigma_sq_hp / 2) + .ok_or(SolMathError::Overflow)?; + let drift_hp = fp_mul_hp_i(drift_rate_hp, t_hp)?; let lambda_sst = if sigma_sqrt_t_hp > 0 { fp_div_hp_safe(drift_hp, sigma_sqrt_t_hp)? } else { @@ -87,7 +102,9 @@ fn compute_intermediates( let mk = |log_val: i128| -> Result { if sigma_sqrt_t_hp > 0 { // fp_div_hp_safe result ∈ [-~1e15, ~1e15]; lambda_sst ∈ [-~1e15, ~1e15] (finite-rate drift); sum ≤ ~2e15, fits i128 - Ok(fp_div_hp_safe(log_val, sigma_sqrt_t_hp)? + lambda_sst) + fp_div_hp_safe(log_val, sigma_sqrt_t_hp)? + .checked_add(lambda_sst) + .ok_or(SolMathError::Overflow) } else { Ok(0) } @@ -97,12 +114,19 @@ fn compute_intermediates( let x1_hp = mk(ln_sh)?; let y1_hp = mk(-ln_sh)?; // -ln_sh ∈ [-~1e15, ~1e15], ln_hk ∈ [-~1e15, ~1e15]; sum ≤ ~2e15, fits i128 - let y_hp = mk(-ln_sh + ln_hk)?; + let y_hp = mk(ln_sh + .checked_neg() + .and_then(|v| v.checked_add(ln_hk)) + .ok_or(SolMathError::Overflow)?)?; // Power terms at HP via exp(2λ·ln(H/S)) let sigma_sq_std = fp_mul_i(sigma as i128, sigma as i128)?; // r as i128 ≤ ~1e12 (rate at SCALE), sigma_sq_std ≤ SCALE (volatility² ≤ 1.0 at SCALE); sum ≤ ~2e12, fits i128 - let two_lambda_std = fp_div_i(2 * (r as i128 + sigma_sq_std / 2), sigma_sq_std)?; + let lambda_num = (r as i128) + .checked_add(sigma_sq_std / 2) + .and_then(|v| v.checked_mul(2)) + .ok_or(SolMathError::Overflow)?; + let two_lambda_std = fp_div_i(lambda_num, sigma_sq_std)?; let two_lambda_hp = upscale_std_to_hp(two_lambda_std as u128)?; // two_lambda_hp ≤ ~100·SCALE_HP (lambda is a dimensionless financial ratio, typically ≤ 100); 2·SCALE_HP ≈ 2e15; no underflow for lambda > 1 let two_lambda_m2_hp = two_lambda_hp - 2 * SCALE_HP; @@ -120,8 +144,16 @@ fn compute_intermediates( }; Ok(HaugIntermediates { - s_hp, k_disc_hp, x1_hp, y1_hp, d1_hp, y_hp, - sigma_sqrt_t_hp, discount_hp, pow_2l_hp, pow_2lm2_hp, + s_hp, + k_disc_hp, + x1_hp, + y1_hp, + d1_hp, + y_hp, + sigma_sqrt_t_hp, + discount_hp, + pow_2l_hp, + pow_2lm2_hp, phi: if is_call { 1 } else { -1 }, }) } @@ -137,10 +169,9 @@ fn block_hp(phi: i128, z: i128, s_eff: i128, k_eff: i128, sst: i128) -> Result Result<(i128, i128, i128, i128), SolMat /// Prices a European option with a single knock-in or knock-out barrier /// using Haug's ABCD decomposition, verified against QuantLib on 443K vectors. /// +/// This formula assumes continuous monitoring, zero rebate, no dividends, and +/// that the barrier has **not** been breached before the valuation instant. +/// On-chain callers with persisted path state should use +/// [`barrier_option_with_state`]. Discretely sampled oracle barriers require a +/// separate monitoring correction and must not be priced as continuous. +/// /// # Parameters /// - `s` -- Spot price at SCALE (u128) /// - `k` -- Strike price at SCALE (u128) @@ -176,7 +213,8 @@ fn all_blocks(im: &HaugIntermediates) -> Result<(i128, i128, i128, i128), SolMat /// Returns `Err(DomainError)` if `s`, `k`, `h`, `sigma`, or `t` are zero. /// /// # Accuracy -/// Max 1.7K ULP, P99 33, median 1. CU: ~160K average. +/// Max 1.7K ULP, P99 33, median 1. Final SBF audit: 270,156 CU +/// average and 415,531 max for this math call. /// /// Public return values preserve exact in/out conservation after rounding. /// @@ -193,8 +231,12 @@ fn all_blocks(im: &HaugIntermediates) -> Result<(i128, i128, i128, i128), SolMat /// # Ok::<(), solmath::SolMathError>(()) /// ``` pub fn barrier_option( - s: u128, k: u128, h: u128, - r: u128, sigma: u128, t: u128, + s: u128, + k: u128, + h: u128, + r: u128, + sigma: u128, + t: u128, is_call: bool, barrier_type: BarrierType, ) -> Result { @@ -202,24 +244,39 @@ pub fn barrier_option( return Err(SolMathError::DomainError); } - let is_down = matches!(barrier_type, BarrierType::DownAndOut | BarrierType::DownAndIn); - let is_out = matches!(barrier_type, BarrierType::DownAndOut | BarrierType::UpAndOut); + let is_down = matches!( + barrier_type, + BarrierType::DownAndOut | BarrierType::DownAndIn + ); + let is_out = matches!( + barrier_type, + BarrierType::DownAndOut | BarrierType::UpAndOut + ); // Already at or past barrier if (is_down && s <= h) || (!is_down && s >= h) { let (call, put) = black_scholes_price_hp(s, k, r, sigma, t)?; let vanilla = if is_call { call } else { put }; - return Ok(BarrierResult { price: if is_out { 0 } else { vanilla }, vanilla }); + return Ok(BarrierResult { + price: if is_out { 0 } else { vanilla }, + vanilla, + }); } // Impossible payoff: up call K≥H, down put K≤H if is_call && !is_down && k >= h { let (call, _) = black_scholes_price_hp(s, k, r, sigma, t)?; - return Ok(BarrierResult { price: if is_out { 0 } else { call }, vanilla: call }); + return Ok(BarrierResult { + price: if is_out { 0 } else { call }, + vanilla: call, + }); } if !is_call && is_down && k <= h { let (_, put) = black_scholes_price_hp(s, k, r, sigma, t)?; - return Ok(BarrierResult { price: if is_out { 0 } else { put }, vanilla: put }); + return Ok(BarrierResult { + price: if is_out { 0 } else { put }, + vanilla: put, + }); } let im = compute_intermediates(s, k, h, r, sigma, t, is_call)?; @@ -244,7 +301,10 @@ pub fn barrier_option( let digital_hp = fp_mul_hp_i( im.discount_hp, norm_cdf_poly_hp(im.sigma_sqrt_t_hp - im.x1_hp)? - - fp_mul_hp_i(im.pow_2lm2_hp, norm_cdf_poly_hp(im.sigma_sqrt_t_hp - im.y1_hp)?)?, + - fp_mul_hp_i( + im.pow_2lm2_hp, + norm_cdf_poly_hp(im.sigma_sqrt_t_hp - im.y1_hp)?, + )?, )?; // p_uo_h_hp ∈ [-~1e20, ~1e20]; fp_mul_hp_i of (k-h) upscaled × digital ∈ [-~1e20, ~1e20]; sum ≤ ~2e20, fits i128 @@ -265,7 +325,80 @@ pub fn barrier_option( let vanilla = downscale_hp_to_std(vanilla_hp); let out_price = core::cmp::min(downscale_hp_to_std(out_hp), vanilla); - let price = if is_out { out_price } else { vanilla - out_price }; + let price = if is_out { + out_price + } else { + vanilla - out_price + }; Ok(BarrierResult { price, vanilla }) } + +/// Path-state-aware barrier pricing. +/// +/// Set `barrier_was_breached` from persisted contract/oracle state. Once +/// breached, a knock-out is worth zero and a knock-in is worth the vanilla +/// option regardless of the current spot. +pub fn barrier_option_with_state( + s: u128, + k: u128, + h: u128, + r: u128, + sigma: u128, + t: u128, + is_call: bool, + barrier_type: BarrierType, + barrier_was_breached: bool, +) -> Result { + if !barrier_was_breached { + return barrier_option(s, k, h, r, sigma, t, is_call, barrier_type); + } + if s == 0 || k == 0 || h == 0 || sigma == 0 || t == 0 { + return Err(SolMathError::DomainError); + } + let (call, put) = black_scholes_price_hp(s, k, r, sigma, t)?; + let vanilla = if is_call { call } else { put }; + let knocked_out = matches!( + barrier_type, + BarrierType::DownAndOut | BarrierType::UpAndOut + ); + Ok(BarrierResult { + price: if knocked_out { 0 } else { vanilla }, + vanilla, + }) +} + +#[cfg(test)] +mod path_state_tests { + use super::*; + + #[test] + fn historical_breach_overrides_current_safe_spot() { + let out = barrier_option_with_state( + 100 * SCALE, + 100 * SCALE, + 90 * SCALE, + 50_000_000_000, + 200_000_000_000, + SCALE, + true, + BarrierType::DownAndOut, + true, + ) + .unwrap(); + let knocked_in = barrier_option_with_state( + 100 * SCALE, + 100 * SCALE, + 90 * SCALE, + 50_000_000_000, + 200_000_000_000, + SCALE, + true, + BarrierType::DownAndIn, + true, + ) + .unwrap(); + assert_eq!(out.price, 0); + assert_eq!(knocked_in.price, knocked_in.vanilla); + } +} diff --git a/src/bs.rs b/src/bs.rs index 2a29d99..c907ef2 100644 --- a/src/bs.rs +++ b/src/bs.rs @@ -1,8 +1,8 @@ +use crate::arithmetic::{european_prices_from_call, fp_div, fp_div_i, fp_mul_i, fp_sqrt}; use crate::constants::*; use crate::error::SolMathError; -use crate::arithmetic::{fp_mul_i, fp_div, fp_div_i, fp_sqrt}; -use crate::transcendental::{ln_fixed_i, exp_fixed_i}; -use crate::normal::{norm_cdf_poly, norm_cdf_and_pdf_bs_guarded}; +use crate::normal::{norm_cdf_and_pdf_bs_guarded, norm_cdf_poly}; +use crate::transcendental::{exp_fixed_i, ln_fixed_i}; // ============================================================ // black_scholes_price: All intermediates in i128 @@ -24,6 +24,7 @@ use crate::normal::{norm_cdf_poly, norm_cdf_and_pdf_bs_guarded}; /// /// # Accuracy /// 6-9 significant figures vs analytic reference. +/// Final SBF audit: 36,919 CU average, 50,964 max. /// /// # Example /// ``` @@ -36,21 +37,35 @@ use crate::normal::{norm_cdf_poly, norm_cdf_and_pdf_bs_guarded}; /// assert!(put > 0); /// # Ok::<(), solmath::SolMathError>(()) /// ``` -pub fn black_scholes_price(s: u128, k: u128, r: u128, sigma: u128, t: u128) -> Result<(u128, u128), SolMathError> { +pub fn black_scholes_price( + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, +) -> Result<(u128, u128), SolMathError> { black_scholes_price_selective(s, k, r, sigma, t) } - /// Selective BS price implementation. Internal. -pub(crate) fn black_scholes_price_selective(s: u128, k: u128, r: u128, sigma: u128, t: u128) -> Result<(u128, u128), SolMathError> { - if s > i128::MAX as u128 || k > i128::MAX as u128 || r > i128::MAX as u128 - || sigma > i128::MAX as u128 || t > i128::MAX as u128 +pub(crate) fn black_scholes_price_selective( + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, +) -> Result<(u128, u128), SolMathError> { + if s > i128::MAX as u128 + || k > i128::MAX as u128 + || r > i128::MAX as u128 + || sigma > i128::MAX as u128 + || t > i128::MAX as u128 { return Err(SolMathError::Overflow); } if s == 0 { let r_t = fp_mul_i(r as i128, t as i128)?; -let k_disc = fp_mul_i(k as i128, exp_fixed_i(-r_t)?)?; + let k_disc = fp_mul_i(k as i128, exp_fixed_i(-r_t)?)?; return Ok((0, if k_disc > 0 { k_disc as u128 } else { 0 })); } if k == 0 { @@ -78,17 +93,20 @@ let k_disc = fp_mul_i(k as i128, exp_fixed_i(-r_t)?)?; let sigma_sq_half = sigma_sq / 2; // r_i ∈ [0, SCALE_I], sigma_sq_half ∈ [0, SCALE_I/2]: sum ≤ 1.5·SCALE_I, well within i128. - let drift = fp_mul_i(r_i + sigma_sq_half, t_i)?; + let drift_rate = r_i + .checked_add(sigma_sq_half) + .ok_or(SolMathError::Overflow)?; + let drift = fp_mul_i(drift_rate, t_i)?; // ln_sk ∈ [-40·SCALE_I, 40·SCALE_I] (ln domain), drift ∈ [-SCALE_I, SCALE_I] after fp_mul_i; // sum ∈ [-41·SCALE_I, 41·SCALE_I], fits i128. - let d1_num = ln_sk + drift; + let d1_num = ln_sk.checked_add(drift).ok_or(SolMathError::Overflow)?; let sqrt_t = fp_sqrt(t)? as i128; let sigma_sqrt_t = fp_mul_i(sigma_i, sqrt_t)?; if sigma_sqrt_t <= 1 { let r_t = fp_mul_i(r_i, t_i)?; -let discount = exp_fixed_i(-r_t)?; + let discount = exp_fixed_i(-r_t)?; let k_disc = fp_mul_i(k_i, discount)?; // s_i is a SCALE price (< ~1e20 in practice), k_disc = k·discount ≤ k ≤ ~1e20; @@ -105,14 +123,11 @@ let discount = exp_fixed_i(-r_t)?; let d1 = fp_div_i(d1_num, sigma_sqrt_t)?; // d1 ∈ [-8·SCALE_I, 8·SCALE_I] (clamped by norm_cdf_bs_guarded); sigma_sqrt_t ∈ [0, ~SCALE_I]; // d2 = d1 - sigma_sqrt_t ∈ [-9·SCALE_I, 8·SCALE_I], fits i128. - let d2 = d1 - sigma_sqrt_t; + let d2 = d1.checked_sub(sigma_sqrt_t).ok_or(SolMathError::Overflow)?; let phi_d1 = norm_cdf_poly(d1)?; // phi_d1 ∈ [0, SCALE_I]; SCALE_I - phi_d1 ∈ [0, SCALE_I], fits i128. - let phi_neg_d1 = SCALE_I - phi_d1; let phi_d2 = norm_cdf_poly(d2)?; - // phi_d2 ∈ [0, SCALE_I]; SCALE_I - phi_d2 ∈ [0, SCALE_I], fits i128. - let phi_neg_d2 = SCALE_I - phi_d2; let r_t = fp_mul_i(r_i, t_i)?; // -r_t is ≤ 0, so exp cannot overflow @@ -121,17 +136,11 @@ let discount = exp_fixed_i(-r_t)?; let term1 = fp_mul_i(s_i, phi_d1)?; let term2 = fp_mul_i(k_disc, phi_d2)?; - // term1, term2 ∈ [0, SCALE_I] (prices after fp_mul_i); difference ∈ (-SCALE_I, SCALE_I), fits i128. - let call_i = term1 - term2; - let call = if call_i > 0 { call_i as u128 } else { 0 }; - - let term3 = fp_mul_i(k_disc, phi_neg_d2)?; - let term4 = fp_mul_i(s_i, phi_neg_d1)?; - // term3, term4 ∈ [0, SCALE_I]; difference ∈ (-SCALE_I, SCALE_I), fits i128. - let put_i = term3 - term4; - let put = if put_i > 0 { put_i as u128 } else { 0 }; - - Ok((call, put)) + // For ordinary prices term1, term2 ∈ [0, SCALE_I] and the difference fits + // trivially; but s/k are only bounded by i128::MAX, so combine with checked + // arithmetic to fail closed instead of over/underflowing on absurd inputs. + let call_i = term1.checked_sub(term2).ok_or(SolMathError::Overflow)?; + european_prices_from_call(call_i, s, k_disc) } // ============================================================ @@ -139,15 +148,29 @@ let discount = exp_fixed_i(-r_t)?; // ============================================================ /// Compute BS intermediates (d1, d2, CDFs, discount). Internal. -pub(crate) fn bs_intermediates(s: u128, k: u128, r: u128, sigma: u128, t: u128) -> Result { +pub(crate) fn bs_intermediates( + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, +) -> Result { bs_intermediates_selective(s, k, r, sigma, t) } - /// Selective BS intermediates. Internal. -pub(crate) fn bs_intermediates_selective(s: u128, k: u128, r: u128, sigma: u128, t: u128) -> Result { - if s > i128::MAX as u128 || k > i128::MAX as u128 || r > i128::MAX as u128 - || sigma > i128::MAX as u128 || t > i128::MAX as u128 +pub(crate) fn bs_intermediates_selective( + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, +) -> Result { + if s > i128::MAX as u128 + || k > i128::MAX as u128 + || r > i128::MAX as u128 + || sigma > i128::MAX as u128 + || t > i128::MAX as u128 { return Err(SolMathError::Overflow); } @@ -164,9 +187,12 @@ pub(crate) fn bs_intermediates_selective(s: u128, k: u128, r: u128, sigma: u128, // sigma_sq ∈ [0, SCALE_I] after fp_mul_i; /2: ∈ [0, SCALE_I/2], fits i128. let sigma_sq_half = sigma_sq / 2; // r_i ∈ [0, SCALE_I], sigma_sq_half ∈ [0, SCALE_I/2]: sum ≤ 1.5·SCALE_I, fits i128. - let drift = fp_mul_i(r_i + sigma_sq_half, t_i)?; + let drift_rate = r_i + .checked_add(sigma_sq_half) + .ok_or(SolMathError::Overflow)?; + let drift = fp_mul_i(drift_rate, t_i)?; // ln_sk ∈ [-40·SCALE_I, 40·SCALE_I], drift ∈ [-SCALE_I, SCALE_I]; sum ∈ [-41·SCALE_I, 41·SCALE_I], fits i128. - let d1_num = ln_sk + drift; + let d1_num = ln_sk.checked_add(drift).ok_or(SolMathError::Overflow)?; let sqrt_t = fp_sqrt(t)? as i128; let sigma_sqrt_t = fp_mul_i(sigma_i, sqrt_t)?; @@ -183,7 +209,7 @@ pub(crate) fn bs_intermediates_selective(s: u128, k: u128, r: u128, sigma: u128, 0 }; // d1 ∈ [-8·SCALE_I, 8·SCALE_I], sigma_sqrt_t ∈ [0, ~SCALE_I]; d2 ∈ [-9·SCALE_I, 8·SCALE_I], fits i128. - let d2 = d1 - sigma_sqrt_t; + let d2 = d1.checked_sub(sigma_sqrt_t).ok_or(SolMathError::Overflow)?; let (phi_d1, pdf_d1) = norm_cdf_and_pdf_bs_guarded(d1)?; let phi_d2 = norm_cdf_poly(d2)?; @@ -211,7 +237,6 @@ pub(crate) fn bs_intermediates_selective(s: u128, k: u128, r: u128, sigma: u128, }) } - /// Black-Scholes vega: S * phi(d1) * sqrt(T) at SCALE. /// /// Returns vega (signed, at SCALE). Same for calls and puts. @@ -225,9 +250,14 @@ pub fn bs_vega(s: u128, k: u128, r: u128, sigma: u128, t: u128) -> Result Result { +pub(crate) fn bs_vega_selective( + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, +) -> Result { if sigma == 0 || t == 0 { return Err(SolMathError::DomainError); } @@ -248,7 +278,13 @@ pub(crate) fn bs_vega_selective(s: u128, k: u128, r: u128, sigma: u128, t: u128) /// /// # Accuracy /// 6-9 significant figures. -pub fn bs_delta(s: u128, k: u128, r: u128, sigma: u128, t: u128) -> Result<(i128, i128), SolMathError> { +pub fn bs_delta( + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, +) -> Result<(i128, i128), SolMathError> { if sigma == 0 || t == 0 { return Err(SolMathError::DomainError); } @@ -276,9 +312,14 @@ pub fn bs_gamma(s: u128, k: u128, r: u128, sigma: u128, t: u128) -> Result Result { +pub(crate) fn bs_gamma_selective( + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, +) -> Result { if sigma == 0 || t == 0 { return Err(SolMathError::DomainError); } @@ -303,13 +344,24 @@ pub(crate) fn bs_gamma_selective(s: u128, k: u128, r: u128, sigma: u128, t: u128 /// /// # Accuracy /// 6-9 significant figures. -pub fn bs_theta(s: u128, k: u128, r: u128, sigma: u128, t: u128) -> Result<(i128, i128), SolMathError> { +pub fn bs_theta( + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, +) -> Result<(i128, i128), SolMathError> { bs_theta_selective(s, k, r, sigma, t) } - /// Selective BS theta. Internal. -pub(crate) fn bs_theta_selective(s: u128, k: u128, r: u128, sigma: u128, t: u128) -> Result<(i128, i128), SolMathError> { +pub(crate) fn bs_theta_selective( + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, +) -> Result<(i128, i128), SolMathError> { if sigma == 0 || t == 0 { return Err(SolMathError::DomainError); } @@ -323,7 +375,7 @@ pub(crate) fn bs_theta_selective(s: u128, k: u128, r: u128, sigma: u128, t: u128 let term1_num = fp_mul_i(fp_mul_i(s_i, im.pdf_d1)?, sigma_i)?; // im.sqrt_t ∈ [0, SCALE_I]; 2 * im.sqrt_t ≤ 2e12, fits i128. - let two_sqrt_t = 2 * im.sqrt_t; + let two_sqrt_t = im.sqrt_t.checked_mul(2).ok_or(SolMathError::Overflow)?; let term1 = if two_sqrt_t > 0 { -fp_div_i(term1_num, two_sqrt_t)? } else { @@ -333,10 +385,14 @@ pub(crate) fn bs_theta_selective(s: u128, k: u128, r: u128, sigma: u128, t: u128 let term2_call = fp_mul_i(r_k_disc, im.phi_d2)?; let term2_put = fp_mul_i(r_k_disc, im.phi_neg_d2)?; - // term1 is negative (≥ -SCALE_I), term2_call ≥ 0; difference ∈ [-2·SCALE_I, 0], fits i128. - let theta_call = term1 - term2_call; - // term1 ∈ [-SCALE_I, 0], term2_put ∈ [0, SCALE_I]; sum ∈ [-SCALE_I, SCALE_I], fits i128. - let theta_put = term1 + term2_put; + // For ordinary inputs term1 ∈ [-SCALE_I, 0] and term2_* ∈ [0, SCALE_I], but + // s/k are only bounded by i128::MAX, so a huge discounted strike can push + // both terms near the i128 limits. Combine with checked arithmetic so an + // out-of-range Greek fails closed instead of overflowing. + let theta_call = term1 + .checked_sub(term2_call) + .ok_or(SolMathError::Overflow)?; + let theta_put = term1.checked_add(term2_put).ok_or(SolMathError::Overflow)?; Ok((theta_call, theta_put)) } @@ -349,13 +405,24 @@ pub(crate) fn bs_theta_selective(s: u128, k: u128, r: u128, sigma: u128, t: u128 /// /// # Accuracy /// 6-9 significant figures. -pub fn bs_rho(s: u128, k: u128, r: u128, sigma: u128, t: u128) -> Result<(i128, i128), SolMathError> { +pub fn bs_rho( + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, +) -> Result<(i128, i128), SolMathError> { bs_rho_selective(s, k, r, sigma, t) } - /// Selective BS rho. Internal. -pub(crate) fn bs_rho_selective(s: u128, k: u128, r: u128, sigma: u128, t: u128) -> Result<(i128, i128), SolMathError> { +pub(crate) fn bs_rho_selective( + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, +) -> Result<(i128, i128), SolMathError> { if sigma == 0 || t == 0 { return Err(SolMathError::DomainError); } @@ -389,6 +456,7 @@ pub(crate) fn bs_rho_selective(s: u128, k: u128, r: u128, sigma: u128, t: u128) /// /// # Accuracy /// 6-9 significant figures. +/// Final SBF audit: 53,983 CU average, 60,251 max for the full result. /// /// # Example /// ``` @@ -406,11 +474,19 @@ pub fn bs_full(s: u128, k: u128, r: u128, sigma: u128, t: u128) -> Result Result { - if s > i128::MAX as u128 || k > i128::MAX as u128 || r > i128::MAX as u128 - || sigma > i128::MAX as u128 || t > i128::MAX as u128 +pub(crate) fn bs_full_selective( + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, +) -> Result { + if s > i128::MAX as u128 + || k > i128::MAX as u128 + || r > i128::MAX as u128 + || sigma > i128::MAX as u128 + || t > i128::MAX as u128 { return Err(SolMathError::Overflow); } @@ -422,12 +498,35 @@ pub(crate) fn bs_full_selective(s: u128, k: u128, r: u128, sigma: u128, t: u128) let r_t = fp_mul_i(r as i128, t as i128)?; let discount = exp_fixed_i(-r_t)?; let k_disc = fp_mul_i(k as i128, discount)?; + let put_theta = if s == 0 { + fp_mul_i(r as i128, k_disc)? + } else { + 0 + }; + let put_rho = if s == 0 { + -fp_mul_i(t as i128, k_disc)? + } else { + 0 + }; return Ok(BsFull { call: if s > 0 { s } else { 0 }, - put: if s == 0 { if k_disc > 0 { k_disc as u128 } else { 0 } } else { 0 }, + put: if s == 0 { + if k_disc > 0 { + k_disc as u128 + } else { + 0 + } + } else { + 0 + }, call_delta: if s == 0 { 0 } else { SCALE_I }, put_delta: if s == 0 { -SCALE_I } else { 0 }, - gamma: 0, vega: 0, call_theta: 0, put_theta: 0, call_rho: 0, put_rho: 0, + gamma: 0, + vega: 0, + call_theta: 0, + put_theta, + call_rho: 0, + put_rho, }); } @@ -440,9 +539,7 @@ pub(crate) fn bs_full_selective(s: u128, k: u128, r: u128, sigma: u128, t: u128) // fp_mul_i terms: s·phi_d1 and k_disc·phi_d2 each ∈ [0, SCALE_I]; difference ∈ (-SCALE_I, SCALE_I), fits i128. let call_i = fp_mul_i(s_i, im.phi_d1)? - fp_mul_i(im.k_disc, im.phi_d2)?; // Similarly: k_disc·phi_neg_d2 and s·phi_neg_d1 each ∈ [0, SCALE_I]; difference ∈ (-SCALE_I, SCALE_I), fits i128. - let put_i = fp_mul_i(im.k_disc, im.phi_neg_d2)? - fp_mul_i(s_i, im.phi_neg_d1)?; - let call = if call_i > 0 { call_i as u128 } else { 0 }; - let put = if put_i > 0 { put_i as u128 } else { 0 }; + let (call, put) = european_prices_from_call(call_i, s, im.k_disc)?; let call_delta = im.phi_d1; // phi_d1 ∈ [0, SCALE_I]; phi_d1 - SCALE_I ∈ [-SCALE_I, 0], fits i128. @@ -459,16 +556,22 @@ pub(crate) fn bs_full_selective(s: u128, k: u128, r: u128, sigma: u128, t: u128) let term1_num = fp_mul_i(fp_mul_i(s_i, im.pdf_d1)?, sigma_i)?; // im.sqrt_t ∈ [0, SCALE_I]; 2 * im.sqrt_t ≤ 2e12, fits i128. - let two_sqrt_t = 2 * im.sqrt_t; + let two_sqrt_t = im.sqrt_t.checked_mul(2).ok_or(SolMathError::Overflow)?; let term1 = if two_sqrt_t > 0 { -fp_div_i(term1_num, two_sqrt_t)? } else { 0 }; let r_k_disc = fp_mul_i(r_i, im.k_disc)?; - // term1 ∈ [-SCALE_I, 0], fp_mul_i terms ∈ [0, SCALE_I]; differences ∈ [-2·SCALE_I, SCALE_I], fits i128. - let call_theta = term1 - fp_mul_i(r_k_disc, im.phi_d2)?; - let put_theta = term1 + fp_mul_i(r_k_disc, im.phi_neg_d2)?; + // For ordinary inputs term1 ∈ [-SCALE_I, 0] and the fp_mul_i terms ∈ [0, + // SCALE_I], but s/k are only bounded by i128::MAX; combine with checked + // arithmetic so an out-of-range Greek fails closed instead of overflowing. + let call_theta = term1 + .checked_sub(fp_mul_i(r_k_disc, im.phi_d2)?) + .ok_or(SolMathError::Overflow)?; + let put_theta = term1 + .checked_add(fp_mul_i(r_k_disc, im.phi_neg_d2)?) + .ok_or(SolMathError::Overflow)?; let kt_disc = fp_mul_i(im.k_disc, t_i)?; let call_rho = fp_mul_i(kt_disc, im.phi_d2)?; diff --git a/src/bvn_cdf.rs b/src/bvn_cdf.rs index 0871e55..b15362d 100644 --- a/src/bvn_cdf.rs +++ b/src/bvn_cdf.rs @@ -1,4 +1,4 @@ -use crate::arithmetic::{fp_div_i, fp_mul_i}; +use crate::arithmetic::{fp_div_i, fp_mul_i, fp_sqrt}; use crate::constants::PI_OVER_2_SCALE; use crate::error::SolMathError; use crate::normal::norm_cdf_poly; @@ -72,15 +72,13 @@ const GL20_WEIGHTS: [i128; 20] = [ ]; const INV_TWO_PI: i128 = 159_154_943_092; -const RHO_NEAR_ONE: i128 = SCALE_I - 1_000_000; // 1 - 1e-6 - #[inline] fn clamp_prob(value: i128) -> i128 { value.clamp(0, SCALE_I) } fn asin_fixed(x: i128) -> Result { - if x.abs() > SCALE_I { + if x < -SCALE_I || x > SCALE_I { return Err(SolMathError::DomainError); } if x == SCALE_I { @@ -90,6 +88,15 @@ fn asin_fixed(x: i128) -> Result { return Ok(-PI_OVER_2_SCALE); } + // Newton loses leverage as cos(theta) approaches zero. Reflect endpoint + // arguments into a well-conditioned small-angle problem. + if x.unsigned_abs() > 990_000_000_000 { + let one_minus_x2 = (SCALE_I - fp_mul_i(x, x)?).max(0); + let small = fp_sqrt(one_minus_x2 as u128)? as i128; + let theta = PI_OVER_2_SCALE - asin_fixed(small)?; + return Ok(if x < 0 { -theta } else { theta }); + } + let x2 = fp_mul_i(x, x)?; let x3 = fp_mul_i(x2, x)?; let x5 = fp_mul_i(x3, x2)?; @@ -187,13 +194,37 @@ fn bvn_cdf_with_gl( nodes: &[i128], weights: &[i128], ) -> Result { - if rho.abs() > SCALE_I { + if rho < -SCALE_I || rho > SCALE_I { return Err(SolMathError::DomainError); } - if rho >= RHO_NEAR_ONE { + if rho == SCALE_I { return norm_cdf_poly(x.min(y)); } - if rho <= -RHO_NEAR_ONE { + if rho == -SCALE_I { + let value = norm_cdf_poly(x)? + .checked_add(norm_cdf_poly(y)?) + .ok_or(SolMathError::Overflow)? + .checked_sub(SCALE_I) + .ok_or(SolMathError::Overflow)?; + return Ok(clamp_prob(value)); + } + + // Near a singular correlation, direct angular GL quadrature develops a + // narrow boundary layer. If the thresholds are separated by more than + // eight conditional standard deviations, the corresponding analytic + // ±1 limit is accurate beyond fixed-point resolution and preserves + // monotonicity at the endpoint. Equal/near-equal thresholds stay on GL. + let rho_sq = fp_mul_i(rho, rho)?; + let conditional_std = fp_sqrt((SCALE_I - rho_sq).max(0) as u128)?; + let separation = if rho >= 0 { + x.abs_diff(y) + } else { + x.checked_add(y).map_or(u128::MAX, i128::unsigned_abs) + }; + if separation > conditional_std.saturating_mul(8) { + if rho >= 0 { + return norm_cdf_poly(x.min(y)); + } let value = norm_cdf_poly(x)? .checked_add(norm_cdf_poly(y)?) .ok_or(SolMathError::Overflow)? @@ -201,6 +232,13 @@ fn bvn_cdf_with_gl( .ok_or(SolMathError::Overflow)?; return Ok(clamp_prob(value)); } + // The angular quadrature develops an unresolved boundary layer for + // near-singular, near-equal thresholds. The validated numerical domain + // ends at |rho| = 0.99; exact endpoints and the analytically safe + // separated-threshold limit above are handled explicitly. + if rho.unsigned_abs() > 990_000_000_000 { + return Err(SolMathError::NoConvergence); + } if x > 0 && y > 0 { let fx = norm_cdf_poly(x)?; @@ -237,7 +275,8 @@ fn bvn_cdf_with_gl( // Public API // ═══════════════════════════════════════════════════════════════ -/// General bivariate normal CDF. Any `ρ`. ~129K CU median, 153K max. +/// General bivariate normal CDF with guarded near-singular correlation. +/// Final SBF audit: 72,018 CU average, 16,922 median, 208,693 max. /// /// Computes `P(X ≤ a, Y ≤ b)` where `(X, Y) ~ N(0, 0, 1, 1, ρ)`. /// @@ -251,19 +290,25 @@ fn bvn_cdf_with_gl( /// - `|ρ| ≤ 0.95`: max error < 5×10⁻⁶ /// - `|ρ| ≤ 0.99`: max error < 10⁻⁴ /// -/// Near `ρ = ±1` the routine switches to the analytic limit. +/// Exact `ρ = ±1` uses the analytic limit. For `0.99 < |ρ| < 1`, unequal +/// thresholds use that limit only when their separation makes its omitted +/// conditional tail smaller than one fixed-point unit; the unresolved +/// near-equal boundary layer returns `NoConvergence`. /// /// # Errors /// /// - `DomainError` if `|rho| > SCALE`. /// - `Overflow` from internal fixed-point operations (extreme inputs). +/// - `NoConvergence` in the unresolved near-singular boundary layer. pub fn bvn_cdf(a: i128, b: i128, rho: i128) -> Result { bvn_cdf_with_gl(a, b, rho, &GL6_NODES, &GL6_WEIGHTS) } -/// High-precision bivariate normal CDF. Any `ρ`. ~331K CU. Accuracy < 10⁻⁶. +/// High-precision bivariate normal CDF with guarded near-singular correlation. +/// Final SBF audit: 163,498 CU average, 16,922 median, 468,417 max. /// -/// 20-point Gauss-Legendre. Use offline for table generation and validation. +/// 20-point Gauss-Legendre. Within `|rho| <= .99`, the fresh reference corpus +/// observed max 123 raw probability units. Use offline for table generation and validation. /// Not recommended on-chain — use [`bvn_cdf`] (GL6) instead. /// /// All inputs/outputs are signed fixed-point `i128` at `SCALE` (1e12). @@ -272,7 +317,39 @@ pub fn bvn_cdf(a: i128, b: i128, rho: i128) -> Result { /// /// - `DomainError` if `|rho| > SCALE`. /// - `Overflow` from internal fixed-point operations (extreme inputs). +/// - `NoConvergence` in the unresolved near-singular boundary layer. pub fn bvn_cdf_hp(a: i128, b: i128, rho: i128) -> Result { bvn_cdf_with_gl(a, b, rho, &GL20_NODES, &GL20_WEIGHTS) } +#[cfg(test)] +mod boundary_tests { + use super::*; + + #[test] + fn minimum_correlation_is_a_domain_error_not_a_panic() { + assert_eq!(bvn_cdf(0, 0, i128::MIN), Err(SolMathError::DomainError)); + assert_eq!(bvn_cdf_hp(0, 0, i128::MIN), Err(SolMathError::DomainError)); + } + + #[test] + fn unresolved_near_perfect_equal_thresholds_fail_closed() { + assert_eq!( + bvn_cdf_hp(0, 0, 999_998_999_999), + Err(SolMathError::NoConvergence) + ); + assert_eq!( + bvn_cdf_hp(0, 0, -999_998_999_999), + Err(SolMathError::NoConvergence) + ); + } + + #[test] + fn near_perfect_correlation_uses_stable_unequal_threshold_limit() { + let expected = norm_cdf_poly(-SCALE_I / 4).unwrap(); + let near = bvn_cdf_hp(-SCALE_I / 4, 0, 999_999_999_999).unwrap(); + let endpoint = bvn_cdf_hp(-SCALE_I / 4, 0, SCALE_I).unwrap(); + assert_eq!(near, expected); + assert_eq!(endpoint, expected); + } +} diff --git a/src/checked.rs b/src/checked.rs new file mode 100644 index 0000000..215cf21 --- /dev/null +++ b/src/checked.rs @@ -0,0 +1,686 @@ +//! Safe-by-construction pricing inputs. +//! +//! The raw pricing functions accept any `u128` and fail closed via `Result` +//! on out-of-range values. This module goes one step further: it makes the +//! *valid financial domain a type*. A [`Price`], [`Rate`], [`Vol`], or [`Time`] +//! can only be constructed inside the certified domain, and the input bundles +//! ([`EuropeanInputs`], [`ImpliedVolInputs`], [`TwapInputs`]) can only be built +//! from those. +//! +//! Once you hold a bundle, the pricing methods **cannot panic and cannot +//! silently wrap**: every internal bound the raw kernels assume is already +//! established by construction. Degenerate-but-in-range corners still fail +//! closed as `Err` — that is the errors-as-values contract, not a fault — but +//! nothing in this layer aborts your instruction. This is the recommended entry +//! point for on-chain programs: validate untrusted instruction data **once** +//! into these types at your program boundary, then thread them through your +//! logic. +//! +//! ``` +//! use solmath::checked::{EuropeanInputs, ImpliedVolInputs}; +//! +//! # fn demo() -> Result<(), solmath::SolMathError> { +//! // Validate raw instruction data once, at the boundary. +//! let inputs = EuropeanInputs::from_raw( +//! 100_000_000_000_000, // s = 100 +//! 105_000_000_000_000, // k = 105 +//! 50_000_000_000, // r = 5% +//! 200_000_000_000, // sigma = 20% +//! 1_000_000_000_000, // t = 1 year +//! )?; +//! let greeks = inputs.full()?; // cannot panic or wrap; Err only on degenerate corners +//! let _ = greeks.call; +//! # Ok(()) +//! # } +//! ``` +//! +//! # Domain +//! +//! The bounds below are deliberately generous — far past any realistic economic +//! value — while remaining inside the region the kernels are proven/measured to +//! handle without overflow. Values larger than these should be rescaled +//! homogeneously (divide `s`, `k`, and the resulting price by a common factor). +//! The bounds are enforced by [`Price::MAX`] etc. and continuously verified by +//! the `checked_inputs_never_panic_over_domain` fuzz test. + +use crate::error::SolMathError; +use crate::SCALE; + +#[cfg(feature = "bs")] +use crate::bs::{black_scholes_price, bs_delta, bs_full, bs_gamma, bs_rho, bs_theta, bs_vega}; +#[cfg(feature = "bs")] +use crate::constants::BsFull; + +/// A non-negative price in fixed-point (`SCALE = 1e12`), bounded to the +/// certified pricing domain `[0, 100_000]` real units. +/// +/// Spot, strike, barrier, and observed option prices are all `Price`s. +#[cfg(any(feature = "bs", feature = "barrier", feature = "asian"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Price(u128); + +/// A non-negative interest rate in fixed-point, bounded to `[0, 1000%]`. +#[cfg(any(feature = "bs", feature = "barrier", feature = "asian"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Rate(u128); + +/// A strictly positive volatility in fixed-point, bounded to `(0, 10000%]`. +/// +/// Black-Scholes requires `sigma > 0`; the type enforces it, so the raw +/// `DomainError` path for zero volatility is unreachable from this layer. +#[cfg(any(feature = "bs", feature = "barrier", feature = "asian"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Vol(u128); + +/// A strictly positive time-to-expiry in years (fixed-point), bounded to +/// `(0, 100]` years. Black-Scholes requires `t > 0`. +#[cfg(any(feature = "bs", feature = "barrier", feature = "asian"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Time(u128); + +#[cfg(any(feature = "bs", feature = "barrier", feature = "asian"))] +impl Price { + /// Upper bound: `100_000` real units (`1e17` raw). This is the price bound + /// the implied-volatility kernel is proven safe against, and it also keeps + /// the Black-Scholes Greek combinations well within `i128`. + pub const MAX: u128 = 100_000 * SCALE; + + /// Construct a validated price. Rejects values above [`Price::MAX`]. + #[inline] + pub const fn new(raw: u128) -> Result { + if raw > Self::MAX { + return Err(SolMathError::DomainError); + } + Ok(Self(raw)) + } + + /// The underlying fixed-point value. + #[inline] + pub const fn get(self) -> u128 { + self.0 + } +} + +#[cfg(any(feature = "bs", feature = "barrier", feature = "asian"))] +impl Rate { + /// Upper bound: `1000%` (`10 * SCALE`). + pub const MAX: u128 = 10 * SCALE; + + /// Construct a validated rate. Rejects values above [`Rate::MAX`]. + #[inline] + pub const fn new(raw: u128) -> Result { + if raw > Self::MAX { + return Err(SolMathError::DomainError); + } + Ok(Self(raw)) + } + + /// The underlying fixed-point value. + #[inline] + pub const fn get(self) -> u128 { + self.0 + } +} + +#[cfg(any(feature = "bs", feature = "barrier", feature = "asian"))] +impl Vol { + /// Upper bound: `10000%` (`100 * SCALE`). + pub const MAX: u128 = 100 * SCALE; + + /// Construct a validated volatility. Requires `0 < raw <= Vol::MAX`. + #[inline] + pub const fn new(raw: u128) -> Result { + if raw == 0 || raw > Self::MAX { + return Err(SolMathError::DomainError); + } + Ok(Self(raw)) + } + + /// The underlying fixed-point value. + #[inline] + pub const fn get(self) -> u128 { + self.0 + } +} + +#[cfg(any(feature = "bs", feature = "barrier", feature = "asian"))] +impl Time { + /// Upper bound: `100` years (`100 * SCALE`). + pub const MAX: u128 = 100 * SCALE; + + /// Construct a validated time-to-expiry. Requires `0 < raw <= Time::MAX`. + #[inline] + pub const fn new(raw: u128) -> Result { + if raw == 0 || raw > Self::MAX { + return Err(SolMathError::DomainError); + } + Ok(Self(raw)) + } + + /// The underlying fixed-point value. + #[inline] + pub const fn get(self) -> u128 { + self.0 + } +} + +/// A validated European-option parameter set: spot, strike, rate, volatility, +/// and time. Every pricing method on this type is guaranteed not to panic or +/// silently wrap for any in-domain input; degenerate corners (e.g. a +/// near-zero spot against a huge strike) fail closed with `Err`. +#[cfg(feature = "bs")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct EuropeanInputs { + s: Price, + k: Price, + r: Rate, + sigma: Vol, + t: Time, +} + +#[cfg(feature = "bs")] +impl EuropeanInputs { + /// Bundle already-validated components. + #[inline] + pub const fn new(s: Price, k: Price, r: Rate, sigma: Vol, t: Time) -> Self { + Self { s, k, r, sigma, t } + } + + /// Validate raw fixed-point instruction data in one shot. Returns + /// `Err(DomainError)` if any field is outside its certified range. + #[inline] + pub const fn from_raw( + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, + ) -> Result { + // `?` is not usable in const fn on the MSRV, so match explicitly. + let s = match Price::new(s) { + Ok(v) => v, + Err(e) => return Err(e), + }; + let k = match Price::new(k) { + Ok(v) => v, + Err(e) => return Err(e), + }; + let r = match Rate::new(r) { + Ok(v) => v, + Err(e) => return Err(e), + }; + let sigma = match Vol::new(sigma) { + Ok(v) => v, + Err(e) => return Err(e), + }; + let t = match Time::new(t) { + Ok(v) => v, + Err(e) => return Err(e), + }; + Ok(Self { s, k, r, sigma, t }) + } + + /// `(call, put)` European prices at SCALE. + #[inline] + pub fn price(&self) -> Result<(u128, u128), SolMathError> { + black_scholes_price(self.s.0, self.k.0, self.r.0, self.sigma.0, self.t.0) + } + + /// Price plus all five Greeks in one call. + #[inline] + pub fn full(&self) -> Result { + bs_full(self.s.0, self.k.0, self.r.0, self.sigma.0, self.t.0) + } + + /// High-precision (1e15 internal) price plus Greeks. + #[cfg(feature = "transcendental")] + #[inline] + pub fn full_hp(&self) -> Result { + crate::hp::bs_full_hp(self.s.0, self.k.0, self.r.0, self.sigma.0, self.t.0) + } + + /// `(call_delta, put_delta)`. + #[inline] + pub fn delta(&self) -> Result<(i128, i128), SolMathError> { + bs_delta(self.s.0, self.k.0, self.r.0, self.sigma.0, self.t.0) + } + + /// Gamma. + #[inline] + pub fn gamma(&self) -> Result { + bs_gamma(self.s.0, self.k.0, self.r.0, self.sigma.0, self.t.0) + } + + /// Vega. + #[inline] + pub fn vega(&self) -> Result { + bs_vega(self.s.0, self.k.0, self.r.0, self.sigma.0, self.t.0) + } + + /// `(call_theta, put_theta)`. + #[inline] + pub fn theta(&self) -> Result<(i128, i128), SolMathError> { + bs_theta(self.s.0, self.k.0, self.r.0, self.sigma.0, self.t.0) + } + + /// `(call_rho, put_rho)`. + #[inline] + pub fn rho(&self) -> Result<(i128, i128), SolMathError> { + bs_rho(self.s.0, self.k.0, self.r.0, self.sigma.0, self.t.0) + } + + /// The validated spot. + #[inline] + pub const fn spot(&self) -> Price { + self.s + } + /// The validated strike. + #[inline] + pub const fn strike(&self) -> Price { + self.k + } +} + +/// A validated implied-volatility problem: an observed market price plus the +/// contract parameters. [`ImpliedVolInputs::solve`] cannot panic; it returns +/// `Ok(sigma)` or `Err(NoConvergence)` for prices with no invertible volatility +/// (a mathematical outcome, not a failure). +#[cfg(feature = "iv")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ImpliedVolInputs { + market_price: Price, + s: Price, + k: Price, + r: Rate, + t: Time, +} + +#[cfg(feature = "iv")] +impl ImpliedVolInputs { + /// Bundle already-validated components. + #[inline] + pub const fn new(market_price: Price, s: Price, k: Price, r: Rate, t: Time) -> Self { + Self { + market_price, + s, + k, + r, + t, + } + } + + /// Validate raw fixed-point instruction data in one shot. + #[inline] + pub const fn from_raw( + market_price: u128, + s: u128, + k: u128, + r: u128, + t: u128, + ) -> Result { + let market_price = match Price::new(market_price) { + Ok(v) => v, + Err(e) => return Err(e), + }; + let s = match Price::new(s) { + Ok(v) => v, + Err(e) => return Err(e), + }; + let k = match Price::new(k) { + Ok(v) => v, + Err(e) => return Err(e), + }; + let r = match Rate::new(r) { + Ok(v) => v, + Err(e) => return Err(e), + }; + let t = match Time::new(t) { + Ok(v) => v, + Err(e) => return Err(e), + }; + Ok(Self { + market_price, + s, + k, + r, + t, + }) + } + + /// Solve for the implied volatility at SCALE. + #[inline] + pub fn solve(&self) -> Result { + crate::iv::implied_vol(self.market_price.0, self.s.0, self.k.0, self.r.0, self.t.0) + } +} + +/// A validated European **barrier**-option parameter set: spot, strike, barrier +/// level, rate, volatility, and time. The barrier `h` is a [`Price`] like spot +/// and strike. Every method is guaranteed not to panic or silently wrap for any +/// in-domain input; degenerate corners fail closed with `Err`. +#[cfg(feature = "barrier")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BarrierInputs { + s: Price, + k: Price, + h: Price, + r: Rate, + sigma: Vol, + t: Time, +} + +#[cfg(feature = "barrier")] +impl BarrierInputs { + /// Bundle already-validated components. + #[inline] + pub const fn new(s: Price, k: Price, h: Price, r: Rate, sigma: Vol, t: Time) -> Self { + Self { + s, + k, + h, + r, + sigma, + t, + } + } + + /// Validate raw fixed-point instruction data in one shot. Returns + /// `Err(DomainError)` if any field is outside its certified range. + #[inline] + pub const fn from_raw( + s: u128, + k: u128, + h: u128, + r: u128, + sigma: u128, + t: u128, + ) -> Result { + let s = match Price::new(s) { + Ok(v) => v, + Err(e) => return Err(e), + }; + let k = match Price::new(k) { + Ok(v) => v, + Err(e) => return Err(e), + }; + let h = match Price::new(h) { + Ok(v) => v, + Err(e) => return Err(e), + }; + let r = match Rate::new(r) { + Ok(v) => v, + Err(e) => return Err(e), + }; + let sigma = match Vol::new(sigma) { + Ok(v) => v, + Err(e) => return Err(e), + }; + let t = match Time::new(t) { + Ok(v) => v, + Err(e) => return Err(e), + }; + Ok(Self { + s, + k, + h, + r, + sigma, + t, + }) + } + + /// Continuously-monitored barrier option price (fresh contract, barrier not + /// yet breached). `barrier_type` selects knock-in/out and up/down. + #[inline] + pub fn price( + &self, + is_call: bool, + barrier_type: crate::barrier::BarrierType, + ) -> Result { + crate::barrier::barrier_option( + self.s.0, + self.k.0, + self.h.0, + self.r.0, + self.sigma.0, + self.t.0, + is_call, + barrier_type, + ) + } + + /// Barrier option price given persisted breach state. Persist + /// `barrier_was_breached` on-chain across observations. + #[inline] + pub fn price_with_state( + &self, + is_call: bool, + barrier_type: crate::barrier::BarrierType, + barrier_was_breached: bool, + ) -> Result { + crate::barrier::barrier_option_with_state( + self.s.0, + self.k.0, + self.h.0, + self.r.0, + self.sigma.0, + self.t.0, + is_call, + barrier_type, + barrier_was_breached, + ) + } +} + +/// A validated continuous arithmetic-Asian / partially fixed TWAP quote. +/// +/// Unlike a vanilla European quote, TWAP validity is relational: the remaining +/// averaging window cannot exceed time to expiry, and the fixed average must be +/// present exactly when the fixed weight is non-zero. Construction checks those +/// relations once; [`Self::price`] then calls the raw kernel with the same +/// validated state. +#[cfg(feature = "asian")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct TwapInputs { + s: Price, + k: Price, + r: Rate, + q: Rate, + sigma: Vol, + t: Time, + averaging_time: u128, + fixed_average: Price, + fixed_weight: u128, +} + +#[cfg(feature = "asian")] +impl TwapInputs { + /// Validate raw fixed-point instruction data in one shot. + #[allow(clippy::too_many_arguments)] + pub const fn from_raw( + s: u128, + k: u128, + r: u128, + q: u128, + sigma: u128, + t: u128, + averaging_time: u128, + fixed_average: u128, + fixed_weight: u128, + ) -> Result { + let s = match Price::new(s) { + Ok(v) if v.get() > 0 => v, + Ok(_) => return Err(SolMathError::DomainError), + Err(e) => return Err(e), + }; + let k = match Price::new(k) { + Ok(v) if v.get() > 0 => v, + Ok(_) => return Err(SolMathError::DomainError), + Err(e) => return Err(e), + }; + let r = match Rate::new(r) { + Ok(v) => v, + Err(e) => return Err(e), + }; + let q = match Rate::new(q) { + Ok(v) => v, + Err(e) => return Err(e), + }; + let sigma = match Vol::new(sigma) { + Ok(v) => v, + Err(e) => return Err(e), + }; + let t = match Time::new(t) { + Ok(v) => v, + Err(e) => return Err(e), + }; + let fixed_average = match Price::new(fixed_average) { + Ok(v) => v, + Err(e) => return Err(e), + }; + + if averaging_time > t.get() || fixed_weight > SCALE { + return Err(SolMathError::DomainError); + } + if fixed_weight < SCALE && averaging_time == 0 { + return Err(SolMathError::DomainError); + } + if fixed_weight == SCALE && averaging_time != 0 { + return Err(SolMathError::DomainError); + } + if fixed_weight == 0 { + if fixed_average.get() != 0 { + return Err(SolMathError::DomainError); + } + } else if fixed_average.get() == 0 { + return Err(SolMathError::DomainError); + } + + Ok(Self { + s, + k, + r, + q, + sigma, + t, + averaging_time, + fixed_average, + fixed_weight, + }) + } + + /// Price the validated partially fixed TWAP state. + #[inline] + pub fn price(&self) -> Result { + crate::asian::twap_option_price( + self.s.0, + self.k.0, + self.r.0, + self.q.0, + self.sigma.0, + self.t.0, + self.averaging_time, + self.fixed_average.0, + self.fixed_weight, + ) + } + + /// Remaining averaging-window length in years at `SCALE`. + #[inline] + pub const fn averaging_time(&self) -> u128 { + self.averaging_time + } + + /// Fraction of the final average already fixed, in `[0, SCALE]`. + #[inline] + pub const fn fixed_weight(&self) -> u128 { + self.fixed_weight + } + + /// Average of the already-fixed observations. + #[inline] + pub const fn fixed_average(&self) -> Price { + self.fixed_average + } +} + +/// A validated weighted-pool swap request. +/// +/// Unlike the option types, the pool's validity is *relational* — it depends on +/// the balance ratio `balance_in / (balance_in + amount_in) >= 0.01` and the +/// weight ratio `weight_in / weight_out <= 20`, not on independent per-field +/// bounds. [`PoolSwapInputs::from_raw`] checks the whole certified domain up +/// front, so holding a value proves the swap is quotable; [`Self::quote`] then +/// cannot fail for a domain reason and cannot panic. Rounding stays +/// protocol-favouring exactly as in [`crate::weighted_pool_swap`]. +#[cfg(feature = "pool")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PoolSwapInputs { + balance_in: u128, + balance_out: u128, + weight_in: u128, + weight_out: u128, + amount_in: u128, + fee_rate: u128, +} + +#[cfg(feature = "pool")] +impl PoolSwapInputs { + /// Validate a raw swap against the certified pool domain. Mirrors the guard + /// chain in [`crate::weighted_pool_swap`]: non-zero balances/weights, a fee + /// in `[0, 100%]`, a weight ratio `w_in / w_out <= 20`, and a post-trade + /// balance ratio `>= 0.01`. `amount_in == 0` is accepted (a no-op quote). + pub fn from_raw( + balance_in: u128, + balance_out: u128, + weight_in: u128, + weight_out: u128, + amount_in: u128, + fee_rate: u128, + ) -> Result { + if weight_out == 0 || weight_in == 0 || balance_in == 0 || balance_out == 0 { + return Err(SolMathError::DomainError); + } + if fee_rate > SCALE { + return Err(SolMathError::DomainError); + } + // Weight ratio must not exceed 20 (exact-integer test, matching kernel). + let weight_q = weight_in / weight_out; + if weight_q > 20 || (weight_q == 20 && weight_in % weight_out != 0) { + return Err(SolMathError::DomainError); + } + // Post-trade balance ratio must be >= 0.01, i.e. balance_in >= + // ceil((balance_in + amount_in) / 100). amount_in == 0 trivially passes. + if amount_in != 0 { + let denominator = balance_in + .checked_add(amount_in) + .ok_or(SolMathError::Overflow)?; + let min_balance = denominator / 100 + u128::from(denominator % 100 != 0); + if balance_in < min_balance { + return Err(SolMathError::DomainError); + } + } + Ok(Self { + balance_in, + balance_out, + weight_in, + weight_out, + amount_in, + fee_rate, + }) + } + + /// Quote the swap: returns `(net_output, fee)` at SCALE. Output rounds down + /// and fee rounds up (protocol-favouring), matching the raw kernel exactly. + #[inline] + pub fn quote(&self) -> Result<(u128, u128), SolMathError> { + crate::pool::weighted_pool_swap( + self.balance_in, + self.balance_out, + self.weight_in, + self.weight_out, + self.amount_in, + self.fee_rate, + ) + } +} diff --git a/src/complex.rs b/src/complex.rs index 0c6bd13..2d291c3 100644 --- a/src/complex.rs +++ b/src/complex.rs @@ -1,5 +1,7 @@ +use crate::arithmetic::{fp_div, fp_div_i, fp_mul, fp_mul_i, fp_mul_round, fp_sqrt}; +use crate::constants::{INV_SQRT2, SCALE, U256}; use crate::error::SolMathError; -use crate::arithmetic::{fp_mul_i, fp_div_i, fp_sqrt}; +use crate::overflow::checked_mul_div_i; use crate::transcendental::exp_fixed_i; use crate::trig::{cos_fixed, sin_fixed}; @@ -9,9 +11,8 @@ use crate::trig::{cos_fixed, sin_fixed}; /// Complex number with real and imaginary parts at SCALE (1e12). /// -/// Used internally by Heston and NIG pricing for characteristic function evaluation. /// Both `re` and `im` are signed fixed-point at SCALE. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Complex { pub re: i128, pub im: i128, @@ -28,58 +29,580 @@ impl Complex { /// Error: ~2–4 ULP. Returns Err(Overflow) on arithmetic overflow. pub fn complex_mul(a: Complex, b: Complex) -> Result { Ok(Complex::new( - fp_mul_i(a.re, b.re)?.checked_sub(fp_mul_i(a.im, b.im)?).ok_or(SolMathError::Overflow)?, - fp_mul_i(a.re, b.im)?.checked_add(fp_mul_i(a.im, b.re)?).ok_or(SolMathError::Overflow)?, + scaled_product_sum(a.re, b.re, a.im, b.im, true)?, + scaled_product_sum(a.re, b.im, a.im, b.re, false)?, )) } +/// Evaluate `(a*b ± c*d) / SCALE` with one final truncation. Combining the +/// full 256-bit products before division avoids false overflow when large +/// terms cancel to a representable complex component. +fn scaled_product_sum( + a: i128, + b: i128, + c: i128, + d: i128, + subtract_second: bool, +) -> Result { + let (negative, quotient) = scaled_product_sum_wide(a, b, c, d, subtract_second)?; + if negative { + if quotient == 1u128 << 127 { + Ok(i128::MIN) + } else if quotient < 1u128 << 127 { + Ok(-(quotient as i128)) + } else { + Err(SolMathError::Overflow) + } + } else if quotient <= i128::MAX as u128 { + Ok(quotient as i128) + } else { + Err(SolMathError::Overflow) + } +} + +fn scaled_product_sum_wide( + a: i128, + b: i128, + c: i128, + d: i128, + subtract_second: bool, +) -> Result<(bool, u128), SolMathError> { + let (negative, magnitude) = product_sum_raw(a, b, c, d, subtract_second)?; + let (quotient, _) = magnitude.div_rem_u64(SCALE as u64); + if quotient.high_u128_nonzero() { + return Err(SolMathError::Overflow); + } + Ok((negative, quotient.low_u128())) +} + +fn product_sum_raw( + a: i128, + b: i128, + c: i128, + d: i128, + subtract_second: bool, +) -> Result<(bool, U256), SolMathError> { + let first = U256::mul_u128(a.unsigned_abs(), b.unsigned_abs()); + let second = U256::mul_u128(c.unsigned_abs(), d.unsigned_abs()); + let first_negative = (a < 0) ^ (b < 0); + let second_negative = (c < 0) ^ (d < 0) ^ subtract_second; + + let (negative, magnitude) = if first_negative == second_negative { + (first_negative, add_u256(first, second)?) + } else { + match first.cmp_words(&second) { + core::cmp::Ordering::Greater | core::cmp::Ordering::Equal => { + let mut difference = first; + let underflow = difference.overflowing_sub_assign(&second); + debug_assert!(!underflow); + (first_negative, difference) + } + core::cmp::Ordering::Less => { + let mut difference = second; + let underflow = difference.overflowing_sub_assign(&first); + debug_assert!(!underflow); + (second_negative, difference) + } + } + }; + + Ok((negative, magnitude)) +} + +fn add_u256(lhs: U256, rhs: U256) -> Result { + let mut limbs = [0u64; 4]; + let mut carry = 0u128; + for (idx, out) in limbs.iter_mut().enumerate() { + let sum = lhs.limbs[idx] as u128 + rhs.limbs[idx] as u128 + carry; + *out = sum as u64; + carry = sum >> 64; + } + if carry != 0 { + Err(SolMathError::Overflow) + } else { + Ok(U256 { limbs }) + } +} + +fn mul_u256_u64(value: U256, factor: u64) -> Result { + let mut limbs = [0u64; 4]; + let mut carry = 0u128; + for (idx, out) in limbs.iter_mut().enumerate() { + let product = value.limbs[idx] as u128 * factor as u128 + carry; + *out = product as u64; + carry = product >> 64; + } + if carry != 0 { + Err(SolMathError::Overflow) + } else { + Ok(U256 { limbs }) + } +} + +fn signed_magnitude_to_i128(negative: bool, magnitude: u128) -> Result { + if negative { + if magnitude == 1u128 << 127 { + Ok(i128::MIN) + } else if magnitude < 1u128 << 127 { + Ok(-(magnitude as i128)) + } else { + Err(SolMathError::Overflow) + } + } else if magnitude <= i128::MAX as u128 { + Ok(magnitude as i128) + } else { + Err(SolMathError::Overflow) + } +} + /// Complex division at SCALE. -/// Error: ~2–4 ULP. Returns Err(DivisionByZero) if b == 0+0i, Err(Overflow) if |b|² overflows. +/// +/// Uses a scaled Smith algorithm and verifies the result by multiplying it +/// back by the divisor. Ill-conditioned cases that cannot meet the backward +/// error bound fail closed with `NoConvergence`. pub fn complex_div(a: Complex, b: Complex) -> Result { - let b_re_sq = fp_mul_i(b.re, b.re)?; - let b_im_sq = fp_mul_i(b.im, b.im)?; - let denom = b_re_sq.checked_add(b_im_sq).ok_or(SolMathError::Overflow)?; - if denom == 0 { + if b.re == 0 && b.im == 0 { return Err(SolMathError::DivisionByZero); } - Ok(Complex::new( - fp_div_i(fp_mul_i(a.re, b.re)?.checked_add(fp_mul_i(a.im, b.im)?).ok_or(SolMathError::Overflow)?, denom)?, - fp_div_i(fp_mul_i(a.im, b.re)?.checked_sub(fp_mul_i(a.re, b.im)?).ok_or(SolMathError::Overflow)?, denom)?, - )) + if a == b { + return Ok(Complex::new(SCALE as i128, 0)); + } + if b.im == 0 { + return verify_division( + a, + b, + Complex::new(fp_div_i(a.re, b.re)?, fp_div_i(a.im, b.re)?), + ); + } + if b.re == 0 { + let im = fp_div_i(a.re, b.im)? + .checked_neg() + .ok_or(SolMathError::Overflow)?; + return verify_division(a, b, Complex::new(fp_div_i(a.im, b.im)?, im)); + } + + if let Some(exact) = exact_complex_div(a, b) { + return exact; + } + + // Each Smith numerator is the sum of two terms whose magnitudes are at + // most the corresponding components of `a`. Halving a very large + // numerator first therefore prevents a representable final quotient from + // failing merely because that intermediate sum exceeds i128. Recovering + // the factor of two costs at most one quotient ULP; the backward check + // below rejects the result if that loss matters for this divisor. + const SAFE_SUM_COMPONENT: u128 = i128::MAX as u128 / 2; + if a.re.unsigned_abs().max(a.im.unsigned_abs()) > SAFE_SUM_COMPONENT { + let half = Complex::new(a.re / 2, a.im / 2); + let half_q = complex_div(half, b)?; + let q = Complex::new( + half_q.re.checked_mul(2).ok_or(SolMathError::Overflow)?, + half_q.im.checked_mul(2).ok_or(SolMathError::Overflow)?, + ); + return verify_division(a, b, q); + } + + // Smith's ratio algorithm avoids both squaring overflow and the + // small-product truncation that made (1+i)/(1+i) collapse to zero. + if b.re.unsigned_abs() >= b.im.unsigned_abs() { + // Form a*(d/c) as the single exact quotient (a*d)/c. Computing d/c + // first would truncate a sub-ULP ratio to zero before a large + // numerator can magnify it. + let den_term = checked_mul_div_i(b.im, b.im, b.re)?; + let den = match b.re.checked_add(den_term) { + Some(den) => den, + None => return divide_with_halved_divisor(a, b), + }; + let re_num = + a.re.checked_add(checked_mul_div_i(a.im, b.im, b.re)?) + .ok_or(SolMathError::Overflow)?; + let im_num = + a.im.checked_sub(checked_mul_div_i(a.re, b.im, b.re)?) + .ok_or(SolMathError::Overflow)?; + let mut re = fp_div_i(re_num, den)?; + let mut im = fp_div_i(im_num, den)?; + if den_term == 0 && b.im != 0 { + re = correct_sub_ulp_denominator(re, b.im, b.re)?; + im = correct_sub_ulp_denominator(im, b.im, b.re)?; + } + verify_division(a, b, Complex::new(re, im)) + } else { + let den_term = checked_mul_div_i(b.re, b.re, b.im)?; + let den = match b.im.checked_add(den_term) { + Some(den) => den, + None => return divide_with_halved_divisor(a, b), + }; + let re_num = checked_mul_div_i(a.re, b.re, b.im)? + .checked_add(a.im) + .ok_or(SolMathError::Overflow)?; + let im_num = checked_mul_div_i(a.im, b.re, b.im)? + .checked_sub(a.re) + .ok_or(SolMathError::Overflow)?; + let mut re = fp_div_i(re_num, den)?; + let mut im = fp_div_i(im_num, den)?; + if den_term == 0 && b.re != 0 { + re = correct_sub_ulp_denominator(re, b.re, b.im)?; + im = correct_sub_ulp_denominator(im, b.re, b.im)?; + } + verify_division(a, b, Complex::new(re, im)) + } +} + +fn exact_complex_div(a: Complex, b: Complex) -> Option> { + // When |b|² fits u128, evaluate the textbook formula exactly with U256 + // cross-products. This covers all ordinary and sub-SCALE inputs, where + // Smith's integer cross-ratios can otherwise discard several output ULP. + let br = b.re.unsigned_abs(); + let bi = b.im.unsigned_abs(); + let denominator = br.checked_mul(br)?.checked_add(bi.checked_mul(bi)?)?; + + Some((|| { + let quotient_component = |x1: i128, y1: i128, x2: i128, y2: i128, subtract: bool| { + let (negative, numerator) = product_sum_raw(x1, y1, x2, y2, subtract)?; + let scaled = mul_u256_u64(numerator, SCALE as u64)?; + let quotient = if denominator <= u64::MAX as u128 { + scaled.div_rem_u64(denominator as u64).0 + } else { + scaled.div_rem_u128(denominator).0 + }; + if quotient.high_u128_nonzero() { + return Err(SolMathError::Overflow); + } + signed_magnitude_to_i128(negative, quotient.low_u128()) + }; + + Ok(Complex::new( + quotient_component(a.re, b.re, a.im, b.im, false)?, + quotient_component(a.im, b.re, a.re, b.im, true)?, + )) + })()) +} + +fn divide_with_halved_divisor(a: Complex, b: Complex) -> Result { + // Smith's denominator has the sign of its dominant component. If its + // two terms overflow when added, both divisor components are large enough + // that halving cannot erase a non-zero component. Dividing by b/2 gives + // twice the desired quotient; truncate that final factor only after the + // stable division, then validate against the original operands. + let half_b = Complex::new(b.re / 2, b.im / 2); + let double_q = complex_div(a, half_b)?; + let q = Complex::new(double_q.re / 2, double_q.im / 2); + verify_division(a, b, q) +} + +fn verify_division(a: Complex, b: Complex, q: Complex) -> Result { + let divisor_l1 = b.re.unsigned_abs().saturating_add(b.im.unsigned_abs()); + // A quotient component may carry up to four raw ULP of documented error. + // Multiplication by the divisor turns that into at most four times its L1 + // magnitude (in raw fixed units), plus the two product truncations. + let tolerance = (divisor_l1 / SCALE).saturating_mul(5).saturating_add(5); + let real_residual = product_sum_residual(q.re, b.re, q.im, b.im, true, a.re)?; + let imag_residual = product_sum_residual(q.re, b.im, q.im, b.re, false, a.im)?; + if real_residual > tolerance || imag_residual > tolerance { + return Err(SolMathError::NoConvergence); + } + Ok(q) +} + +fn product_sum_residual( + a: i128, + b: i128, + c: i128, + d: i128, + subtract_second: bool, + target: i128, +) -> Result { + let (negative, magnitude) = scaled_product_sum_wide(a, b, c, d, subtract_second)?; + let target_magnitude = target.unsigned_abs(); + if magnitude == 0 || target_magnitude == 0 || negative == (target < 0) { + Ok(magnitude.abs_diff(target_magnitude)) + } else { + Ok(magnitude.saturating_add(target_magnitude)) + } +} + +fn sqrt_half_sum(a: u128, b: u128) -> Result { + if (a ^ b) & 1 == 1 { + // (a+b)/2 contains a half raw unit. Compute sqrt(a+b)/sqrt(2) + // so that bit is not truncated before the square root. + if let Some(sum) = a.checked_add(b) { + fp_mul_round(fp_sqrt(sum)?, INV_SQRT2) + } else { + // At this magnitude, discarding the half raw input unit is far + // below one output ULP. Halve first to keep the sum representable. + fp_sqrt(a / 2 + b / 2) + } + } else { + let half = a / 2 + b / 2 + (a % 2 + b % 2) / 2; + fp_sqrt(half) + } +} + +fn correct_sub_ulp_denominator( + quotient: i128, + minor: i128, + major: i128, +) -> Result { + // 1/(1+r²) = 1-r²+O(r⁴). This path is used only when minor²/major + // truncates to zero, so |r| < 1e-6 and the omitted r⁴ term is below one + // raw output unit even at i128-scale quotients. + let first = checked_mul_div_i(quotient, minor, major)?; + let correction = checked_mul_div_i(first, minor, major)?; + quotient + .checked_sub(correction) + .ok_or(SolMathError::Overflow) } /// Complex exponential: exp(a + bi) = exp(a) × (cos(b) + i·sin(b)). /// Error: ~2–4 ULP. Returns Err(Overflow) if exp(z.re) overflows. pub fn complex_exp(z: Complex) -> Result { let e = exp_fixed_i(z.re)?; - Ok(Complex::new(fp_mul_i(e, cos_fixed(z.im)?)?, fp_mul_i(e, sin_fixed(z.im)?)?)) + Ok(Complex::new( + fp_mul_i(e, cos_fixed(z.im)?)?, + fp_mul_i(e, sin_fixed(z.im)?)?, + )) } /// Principal complex square root (re ≥ 0 branch). /// Error: ~2–4 ULP. Returns Err(Overflow) if |z|² overflows, Err from internal division in degenerate cases. pub fn complex_sqrt(z: Complex) -> Result { - let a_sq = fp_mul_i(z.re, z.re)?; - let b_sq = fp_mul_i(z.im, z.im)?; - let norm_sq = a_sq.checked_add(b_sq).ok_or(SolMathError::Overflow)?; - if norm_sq == 0 { + let a = z.re.unsigned_abs(); + let b = z.im.unsigned_abs(); + let magnitude = a.max(b); + if magnitude == 0 { return Ok(Complex::new(0, 0)); } - let modz = fp_sqrt(norm_sq as u128)? as i128; - // modz = |z| ≥ |re| ≥ z.re by definition of modulus; sum ≤ 2·modz ≤ ~20·SCALE_I, fits i128; /2 is safe - let re_arg = (modz + z.re) / 2; - let re = if re_arg > 0 { - fp_sqrt(re_arg as u128)? as i128 + if magnitude < SCALE { + // The normalized-hypot path cannot resolve both components when the + // raw input itself is sub-SCALE. Scale z by 4^n, take the root at a + // well-resolved magnitude, then divide the root by 2^n. + let mut scaled = z; + let mut root_divisor = 1i128; + while scaled.re.unsigned_abs().max(scaled.im.unsigned_abs()) < SCALE { + scaled.re = scaled.re.checked_mul(4).ok_or(SolMathError::Overflow)?; + scaled.im = scaled.im.checked_mul(4).ok_or(SolMathError::Overflow)?; + root_divisor = root_divisor.checked_mul(2).ok_or(SolMathError::Overflow)?; + } + let scaled_root = complex_sqrt(scaled)?; + return Ok(Complex::new( + div_round_i(scaled_root.re, root_divisor)?, + div_round_i(scaled_root.im, root_divisor)?, + )); + } + + // Scaled hypot: squaring tiny components at SCALE can truncate to zero, + // while squaring large components can overflow. Dividing by the largest + // component first avoids both failure modes. + let ratio = fp_div(a.min(b), magnitude)?; + let ratio_sq = fp_mul(ratio, ratio)?; + let unit_hypot = fp_sqrt(SCALE.checked_add(ratio_sq).ok_or(SolMathError::Overflow)?)?; + let modz_u = fp_mul(magnitude, unit_hypot)?; + if z.re >= 0 { + // modz ≥ |re|, both terms non-negative: no cancellation here. + let re_u = z.re as u128; + let re = sqrt_half_sum(modz_u, re_u)? as i128; + if re == 0 { + // Pure imaginary: sqrt(bi) = sqrt(|b|/2)(1 + i·sign(b)) + // modz ≥ |z.re| by definition of modulus, so modz - z.re ≥ 0; no underflow; /2 is safe + let im = sqrt_half_sum(modz_u, z.re.unsigned_abs())? as i128; + return refine_large_sqrt( + z, + Complex::new(0, if z.im < 0 { -im } else { im }), + magnitude, + ); + } + // re ∈ [0, ~sqrt(10)·SCALE_I] (since |z| ≤ ~10·SCALE_I); 2*re ≤ ~2*sqrt(10)*SCALE_I ≈ 6.3e12, fits i128 + let im = fp_div_i(z.im, 2 * re)?; + refine_large_sqrt(z, Complex::new(re, im), magnitude) } else { - 0 - }; - if re == 0 { - // Pure imaginary: sqrt(bi) = sqrt(|b|/2)(1 + i·sign(b)) - // modz ≥ |z.re| by definition of modulus, so modz - z.re ≥ 0; no underflow; /2 is safe - let im_arg = (modz - z.re) / 2; - let im = fp_sqrt(im_arg as u128)? as i128; - return Ok(Complex::new(0, if z.im < 0 { -im } else { im })); - } - // re ∈ [0, ~sqrt(10)·SCALE_I] (since |z| ≤ ~10·SCALE_I); 2*re ≤ ~2*sqrt(10)*SCALE_I ≈ 6.3e12, fits i128 - let im = fp_div_i(z.im, 2 * re)?; - Ok(Complex::new(re, im)) + // z.re < 0: (modz + z.re)/2 cancels catastrophically and zeroes the + // real part. Compute the imaginary part first from (modz − z.re)/2 + // (both terms positive), then recover re from 2·re·im = z.im. + let abs_re = z.re.unsigned_abs(); + let im_mag_u = sqrt_half_sum(modz_u, abs_re)?; + if im_mag_u > i128::MAX as u128 { + return Err(SolMathError::Overflow); + } + let im_mag = im_mag_u as i128; + if im_mag == 0 { + return Ok(Complex::new(0, 0)); // |z| rounded to zero + } + // re = |z.im| / (2·im_mag) ≥ 0 keeps the principal branch; the sign + // of im carries z.im's sign so 2·re·im == z.im. z.im != i128::MIN + // here (its square above would have overflowed). + let two_im = im_mag.checked_mul(2).ok_or(SolMathError::Overflow)?; + let re = if z.im < 0 { + fp_div_i(z.im, -two_im)? + } else { + fp_div_i(z.im, two_im)? + }; + refine_large_sqrt( + z, + Complex::new(re, if z.im < 0 { -im_mag } else { im_mag }), + magnitude, + ) + } +} + +fn div_round_i(value: i128, divisor: i128) -> Result { + let half = divisor / 2; + if value >= 0 { + value + .checked_add(half) + .map(|v| v / divisor) + .ok_or(SolMathError::Overflow) + } else { + value + .checked_sub(half) + .map(|v| v / divisor) + .ok_or(SolMathError::Overflow) + } +} + +fn refine_large_sqrt( + z: Complex, + mut root: Complex, + magnitude: u128, +) -> Result { + // The SCALE-precision normalized hypot is already within a few output + // ULP while |z| is modest. At very large raw magnitudes its sub-ULP + // error gets amplified by sqrt(|z|), so refine w via + // w <- (w + z/w)/2. One step squares the relative seed error; at + // i128-scale the remaining error is dominated by the division's few raw + // ULP rather than by the normalized-hypot seed. + if magnitude <= 16 * SCALE { + return Ok(root); + } + let reciprocal = complex_div(z, root)?; + root = Complex::new( + root.re + .checked_add(reciprocal.re) + .ok_or(SolMathError::Overflow)? + / 2, + root.im + .checked_add(reciprocal.im) + .ok_or(SolMathError::Overflow)? + / 2, + ); + Ok(root) +} + +#[cfg(test)] +mod adversarial_tests { + use super::*; + use crate::constants::SCALE_I; + + #[test] + fn sqrt_preserves_one_raw_ulp() { + assert_eq!(complex_sqrt(Complex::new(1, 0)).unwrap().re, 1_000_000); + } + + #[test] + fn division_by_small_nonzero_value_is_not_zero_division() { + let q = complex_div(Complex::new(SCALE_I, 0), Complex::new(1, 0)).unwrap(); + assert_eq!(q, Complex::new(SCALE_I * SCALE_I, 0)); + } + + #[test] + fn division_preserves_tiny_numerator_and_denominator() { + assert_eq!( + complex_div(Complex::new(1, 1), Complex::new(1, 1)).unwrap(), + Complex::new(SCALE_I, 0) + ); + let q = complex_div(Complex::new(1, 0), Complex::new(1, 1)).unwrap(); + assert!((q.re - SCALE_I / 2).abs() <= 1); + assert!((q.im + SCALE_I / 2).abs() <= 1); + + assert_eq!( + complex_div(Complex::new(1, 2), Complex::new(3, 4)), + Ok(Complex::new(440_000_000_000, 80_000_000_000)), + ); + } + + #[test] + fn imaginary_axis_negation_is_checked() { + assert_eq!( + complex_div(Complex::new(i128::MIN, 0), Complex::new(0, SCALE_I),), + Err(SolMathError::Overflow) + ); + } + + #[test] + fn sqrt_preserves_half_raw_unit() { + let q = complex_sqrt(Complex::new(0, 1)).unwrap(); + assert!(q.re.abs_diff(707_107) <= 1, "q={q:?}"); + assert!(q.im.abs_diff(707_107) <= 1, "q={q:?}"); + } + + #[test] + fn division_preserves_sub_ulp_denominator_ratio_before_magnification() { + let q = complex_div(Complex::new(0, i128::MAX), Complex::new(2 * SCALE_I, 1)).unwrap(); + let expected_re = 42_535_295_865_117_307_932_921_815i128; + let expected_im = 85_070_591_730_234_615_865_843_630_590_294_120_304i128; + assert!(q.re.abs_diff(expected_re) <= 16, "q={q:?}"); + assert!(q.im.abs_diff(expected_im) <= 16, "q={q:?}"); + } + + #[test] + fn sqrt_large_opposite_parity_does_not_false_overflow() { + let q = complex_sqrt(Complex::new(i128::MAX - 1, i128::MAX)).unwrap(); + let expected = Complex::new( + 14_331_035_423_661_364_728_695_749, + 5_936_109_235_329_791_326_823_897, + ); + assert!(q.re.abs_diff(expected.re) <= 2, "q={q:?}"); + assert!(q.im.abs_diff(expected.im) <= 2, "q={q:?}"); + } + + #[test] + fn multiplication_combines_wide_products_before_range_check() { + let z = Complex::new( + 14_331_035_423_661_364_728_695_748, + 5_936_109_235_329_791_326_823_896, + ); + assert_eq!( + complex_mul(z, z), + Ok(Complex::new( + 170_141_183_460_469_231_731_687_274_811_518_222_572, + 170_141_183_460_469_231_731_687_270_436_583_769_475, + )), + ); + } + + #[test] + fn division_large_equal_operands_do_not_false_overflow() { + for z in [ + Complex::new(i128::MAX, i128::MAX), + Complex::new(i128::MAX / 2 + 1, i128::MAX / 2 + 1), + Complex::new(i128::MIN, i128::MIN), + Complex::new(i128::MIN, i128::MAX), + Complex::new(i128::MAX, i128::MIN), + ] { + assert_eq!(complex_div(z, z), Ok(Complex::new(SCALE_I, 0)), "z={z:?}",); + } + } + + #[test] + fn division_large_near_equal_operands_is_backward_stable() { + let a = Complex::new(i128::MAX, i128::MAX - 1); + let b = Complex::new(i128::MAX, i128::MAX); + let q = complex_div(a, b).unwrap(); + assert!(q.re.abs_diff(SCALE_I) <= 1, "q={q:?}"); + assert!(q.im.abs_diff(0) <= 1, "q={q:?}"); + } + + #[test] + fn division_mixed_scale_representable_quotient_does_not_false_overflow() { + let a = Complex::new(0, i128::MAX / 10 * 7); + let b = Complex::new(490_000_000_000, 490_000_000_000); + let expected = 121_529_416_757_478_022_665_490_931_225_631_504_085; + assert_eq!(complex_div(a, b), Ok(Complex::new(expected, expected))); + } + + #[test] + fn sqrt_negative_real_avoids_cancellation() { + let q = complex_sqrt(Complex::new(-4 * SCALE_I, 1_000_000)).unwrap(); + assert!(q.re > 0); + assert!((q.im - 2 * SCALE_I).abs() <= 2); + } } diff --git a/src/constants.rs b/src/constants.rs index d6fbf47..3a2651c 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -84,15 +84,29 @@ pub const LN2_HP_LO: i128 = 309_417_232_121_458; /// Remez degree-7 ln polynomial as array for compensated evaluation. /// Same coefficients as LN_REMEZ_W0..W7, ascending order. pub const LN_REMEZ_COEFFS: [i128; 8] = [ - LN_REMEZ_W0, LN_REMEZ_W1, LN_REMEZ_W2, LN_REMEZ_W3, - LN_REMEZ_W4, LN_REMEZ_W5, LN_REMEZ_W6, LN_REMEZ_W7, + LN_REMEZ_W0, + LN_REMEZ_W1, + LN_REMEZ_W2, + LN_REMEZ_W3, + LN_REMEZ_W4, + LN_REMEZ_W5, + LN_REMEZ_W6, + LN_REMEZ_W7, ]; /// HP Remez degree-9 ln polynomial as array for compensated evaluation. /// Same coefficients as LN_REMEZ_HP0..HP9, ascending order. pub const LN_REMEZ_HP_COEFFS: [i128; 10] = [ - LN_REMEZ_HP0, LN_REMEZ_HP1, LN_REMEZ_HP2, LN_REMEZ_HP3, LN_REMEZ_HP4, - LN_REMEZ_HP5, LN_REMEZ_HP6, LN_REMEZ_HP7, LN_REMEZ_HP8, LN_REMEZ_HP9, + LN_REMEZ_HP0, + LN_REMEZ_HP1, + LN_REMEZ_HP2, + LN_REMEZ_HP3, + LN_REMEZ_HP4, + LN_REMEZ_HP5, + LN_REMEZ_HP6, + LN_REMEZ_HP7, + LN_REMEZ_HP8, + LN_REMEZ_HP9, ]; // Remez rational coefficients for exp. @@ -131,6 +145,79 @@ pub struct U256 { pub limbs: [u64; 4], } +/// Assemble a 256-bit product from the four exact 64×64 partial products. +/// +/// Keeping carry propagation separate from partial-product generation gives +/// the formal harness a solver-sized boundary: the harness can quantify over +/// every valid partial product and prove each output limb independently. +#[inline] +fn assemble_u128_product(p00: u128, p01: u128, p10: u128, p11: u128) -> U256 { + const MASK: u128 = u64::MAX as u128; + + // Assemble one base-2^64 column at a time. Every valid column is below + // 3*2^64, so it fits comfortably in u128 even though the unsplit middle + // partial products would not fit when added directly. + let column1 = (p00 >> 64) + (p01 & MASK) + (p10 & MASK); + let column2 = (p01 >> 64) + (p10 >> 64) + (p11 & MASK) + (column1 >> 64); + let column3 = (p11 >> 64) + (column2 >> 64); + + U256 { + limbs: [p00 as u64, column1 as u64, column2 as u64, column3 as u64], + } +} + +/// One base-2^64 long-division step. +/// +/// The precondition `remainder < divisor` makes the quotient digit fit in one +/// limb. `U256::div_rem_u64` applies this exact transition four times. +#[inline] +fn div_rem_u64_step(remainder: u64, limb: u64, divisor: u64) -> (u64, u64) { + debug_assert!(divisor != 0); + debug_assert!(remainder < divisor); + let current = ((remainder as u128) << 64) | limb as u128; + ( + (current / divisor as u128) as u64, + (current % divisor as u128) as u64, + ) +} + +/// Knuth Algorithm D quotient-digit refinement for a normalized two-limb +/// divisor. Normalization guarantees `v1 >= 2^63`, so at most two decrements +/// are required before the trial digit is valid or `rhat` crosses the base. +#[inline] +fn refine_knuth_quotient_digit( + mut qhat: u128, + mut rhat: u128, + u0: u64, + v0: u64, + v1: u64, +) -> (u128, u128) { + const BASE: u128 = 1u128 << 64; + debug_assert!(qhat < BASE); + debug_assert!(u128::from(v1) >= BASE / 2); + + if rhat < BASE { + while qhat * u128::from(v0) > (rhat << 64) + u128::from(u0) { + debug_assert!(qhat > 0); + (qhat, rhat) = advance_knuth_refinement(qhat, rhat, v1); + if rhat >= BASE { + break; + } + } + } + (qhat, rhat) +} + +/// Apply the arithmetic state transition for one Knuth-D3 correction. +#[inline] +fn advance_knuth_refinement(qhat: u128, rhat: u128, v1: u64) -> (u128, u128) { + const BASE: u128 = 1u128 << 64; + debug_assert!(qhat > 0); + debug_assert!(rhat < BASE); + debug_assert!(u128::from(v1) >= BASE / 2); + (qhat - 1, rhat + u128::from(v1)) +} + impl U256 { #[inline] pub const fn zero() -> Self { @@ -212,24 +299,7 @@ impl U256 { let b0 = b as u64 as u128; let b1 = (b >> 64) as u64 as u128; - let t0 = a0 * b0; - let p0 = t0 as u64; - let carry0 = t0 >> 64; - - // Middle column: a0*b1 + a1*b0 + carry0 can exceed u128. - // Use overflowing_add to capture carry bits. - let (t1_a, c1) = (a0 * b1).overflowing_add(a1 * b0); - let (t1, c2) = t1_a.overflowing_add(carry0); - let p1 = t1 as u64; - let carry1 = (t1 >> 64) + (c1 as u128 + c2 as u128) * (1u128 << 64); - - let t2 = a1 * b1 + carry1; - let p2 = t2 as u64; - let p3 = (t2 >> 64) as u64; - - Self { - limbs: [p0, p1, p2, p3], - } + assemble_u128_product(a0 * b0, a0 * b1, a1 * b0, a1 * b1) } /// Lexicographic comparison of two U256 values. @@ -254,17 +324,13 @@ impl U256 { #[inline] pub fn div_rem_u64(&self, divisor: u64) -> (Self, u64) { let mut quo = [0u64; 4]; - let mut rem = 0u128; - let divisor_u128 = divisor as u128; + let mut rem = 0u64; for i in (0..4).rev() { - let limb = self.limbs[i] as u128; - let cur = (rem << 64) | limb; - quo[i] = (cur / divisor_u128) as u64; - rem = cur % divisor_u128; + (quo[i], rem) = div_rem_u64_step(rem, self.limbs[i], divisor); } - (Self { limbs: quo }, rem as u64) + (Self { limbs: quo }, rem) } /// Divide U256 by u128, returning (quotient, remainder). @@ -313,18 +379,12 @@ impl U256 { rhat = numerator_hat - qhat * v1_u128; } - // Knuth D3 refinement: only enter when rhat < BASE, because - // rhat << 64 overflows u128 when rhat >= 2^64, and in that case - // qhat * v0 < BASE^2 ≤ rhat * BASE so the condition is always false. - if rhat < BASE { - while qhat * v0_u128 > (rhat << 64) + u0 { - qhat -= 1; - rhat += v1_u128; - if rhat >= BASE { - break; - } - } - } + // Knuth D3 refinement. The helper's postcondition proves the loop + // requires at most two decrements and keeps every expression in + // u128. Once rhat >= BASE the refinement inequality is necessarily + // false, so evaluating rhat << 64 is neither needed nor valid. + let (refined_qhat, _) = refine_knuth_quotient_digit(qhat, rhat, u0 as u64, v0, v1); + qhat = refined_qhat; let mut carry = 0u128; let mut borrow = 0u128; @@ -380,6 +440,90 @@ impl U256 { } } +#[cfg(kani)] +mod verification { + use super::*; + + /// Prove the carry assembler returns the four exact base-2^64 columns for + /// every tuple of valid 64×64 partial products. Since `mul_u128` obtains + /// those partial products with non-overflowing u128 multiplication, this + /// is the exact 128×128→256 multiplication postcondition. + #[kani::proof] + fn u256_product_carry_assembly_is_exact() { + const BASE: u128 = 1u128 << 64; + const MASK: u128 = BASE - 1; + const MAX_PARTIAL: u128 = MASK * MASK; + + let p00: u128 = kani::any(); + let p01: u128 = kani::any(); + let p10: u128 = kani::any(); + let p11: u128 = kani::any(); + kani::assume(p00 <= MAX_PARTIAL); + kani::assume(p01 <= MAX_PARTIAL); + kani::assume(p10 <= MAX_PARTIAL); + kani::assume(p11 <= MAX_PARTIAL); + + let product = assemble_u128_product(p00, p01, p10, p11); + + // Independent overflowing-add specification: add the unsplit middle + // products and restore each captured 2^128 carry at the next column. + let carry0 = p00 >> 64; + let (middle_a, carry_a) = p01.overflowing_add(p10); + let (middle, carry_b) = middle_a.overflowing_add(carry0); + let carry1 = (middle >> 64) + (u128::from(carry_a) + u128::from(carry_b)) * BASE; + let upper = p11 + carry1; + + assert_eq!(product.limbs[0], (p00 & MASK) as u64); + assert_eq!(product.limbs[1], (middle & MASK) as u64); + assert_eq!(product.limbs[2], (upper & MASK) as u64); + assert_eq!(product.limbs[3], (upper >> 64) as u64); + } + + /// Prove the concatenated dividend of one long-division step is strictly + /// below `divisor * 2^64`, so the quotient digit fits exactly in one limb. + /// Rust's unsigned `/` and `%` guarantees then supply the reconstruction + /// identity and proper-remainder property for the production step. + #[kani::proof] + fn u256_div_rem_u64_digit_fits_one_limb() { + let remainder: u64 = kani::any(); + let limb: u64 = kani::any(); + let divisor: u64 = kani::any(); + kani::assume(divisor != 0); + kani::assume(remainder < divisor); + + let current = ((remainder as u128) << 64) | limb as u128; + let exclusive_bound = (divisor as u128) << 64; + assert!(current < exclusive_bound); + } + + /// Prove two normalized Knuth-D3 correction transitions necessarily move + /// `rhat` across the base. The production loop therefore performs at most + /// two decrements; every transition is also free of underflow and overflow + /// over its complete precondition domain. + #[kani::proof] + fn knuth_d3_two_corrections_cross_the_base() { + const BASE: u128 = 1u128 << 64; + + let qhat: u64 = kani::any(); + let rhat: u128 = kani::any(); + let v1: u64 = kani::any(); + kani::assume(qhat > 0); + kani::assume(rhat < BASE); + kani::assume(v1 >= 1u64 << 63); + + let (qhat1, rhat1) = advance_knuth_refinement(u128::from(qhat), rhat, v1); + assert!(rhat1 < 2 * BASE); + assert_eq!(qhat1 + 1, u128::from(qhat)); + + if rhat1 < BASE && qhat1 > 0 { + let (qhat2, rhat2) = advance_knuth_refinement(qhat1, rhat1, v1); + assert!(rhat2 >= BASE); + assert!(rhat2 < 2 * BASE); + assert_eq!(qhat2 + 2, u128::from(qhat)); + } + } +} + // ============================================================ // Gauss-Legendre quadrature nodes and weights (test-only) // Generated by scripts/gauss_legendre_coefficients.py — DO NOT EDIT @@ -431,8 +575,10 @@ pub const PI_SCALE: i128 = 3_141_592_653_590; pub const PI_OVER_2_SCALE: i128 = 1_570_796_326_795; pub const PI_OVER_4_SCALE: i128 = 785_398_163_397; pub const TWO_PI_SCALE: i128 = 6_283_185_307_180; -pub const TWO_PI_HI: i128 = TWO_PI_SCALE; -pub const TWO_PI_LO: i128 = -413_500_000_000; +/// Sub-ULP residual of TWO_PI_SCALE: 2π·SCALE = TWO_PI_SCALE + TWO_PI_LO/SCALE. +/// 2π·1e12 = 6283185307179.5864769253…, so TWO_PI_SCALE overshoots by +/// 0.413523074713… ULP. Used by the trig range reduction (Cody-Waite). +pub const TWO_PI_LO: i128 = -413_523_074_713; // sin(x)/x = SIN_C1 + SIN_C3·u + ... + SIN_C11·u⁵ where u = x² pub const SIN_C1: i128 = 1_000_000_000_000; @@ -474,7 +620,7 @@ pub const POLY_I0_HI: i128 = 500_000_000_000; // 0.5 pub const POLY_I1_HI: i128 = 1_500_000_000_000; // 1.5 pub const POLY_I2_HI: i128 = 3_000_000_000_000; // 3.0 pub const POLY_I3_HI: i128 = 5_000_000_000_000; // 5.0 - // Piece 4 upper = 8.0 (handled by outer clamp) + // Piece 4 upper = 8.0 (handled by outer clamp) // Interval midpoints and half-widths (scaled by 1e12) pub const POLY_I0_MID: i128 = 250_000_000_000; // 0.25 @@ -568,69 +714,6 @@ pub const POLY_I4: [i128; 12] = [ 36_710, // 3.670970644971754e-08 ]; -// ============================================================ -// V2 CDF: 6 polynomial pieces + CF8 tail (x >= 5) -// Rounding Horner, boundary-constrained, coordinate-descent optimized. -// Max 5 ULP, monotone, zero boundary discontinuity. -// Generated by scripts/phi_7piece.py -// ============================================================ - -pub const POLY_V2_I0_HI: i128 = 500_000_000_000; -pub const POLY_V2_I1_HI: i128 = 1_500_000_000_000; -pub const POLY_V2_I2_HI: i128 = 2_250_000_000_000; -pub const POLY_V2_I3_HI: i128 = 3_000_000_000_000; -pub const POLY_V2_I4_HI: i128 = 4_000_000_000_000; -// Piece 5 upper = 5.0 (tail takes over) - -pub const POLY_V2_I0_MID: i128 = 250_000_000_000; -pub const POLY_V2_I0_HW: i128 = 250_000_000_000; -pub const POLY_V2_I1_MID: i128 = 1_000_000_000_000; -pub const POLY_V2_I1_HW: i128 = 500_000_000_000; -pub const POLY_V2_I2_MID: i128 = 1_875_000_000_000; -pub const POLY_V2_I2_HW: i128 = 375_000_000_000; -pub const POLY_V2_I3_MID: i128 = 2_625_000_000_000; -pub const POLY_V2_I3_HW: i128 = 375_000_000_000; -pub const POLY_V2_I4_MID: i128 = 3_500_000_000_000; -pub const POLY_V2_I4_HW: i128 = 500_000_000_000; -pub const POLY_V2_I5_MID: i128 = 4_500_000_000_000; -pub const POLY_V2_I5_HW: i128 = 500_000_000_000; - -// Piece 0: [0.0, 0.5] — max 3 ULP -pub const POLY_V2_I0: [i128; 12] = [ - 598_706_325_685, 96_667_029_200, -3_020_844_663, -944_013_957, - 46_217_351, 8_272_417, -471_323, -57_350, 3_612, 332, -25, -5, -]; -// Piece 1: [0.5, 1.5] — max 5 ULP -pub const POLY_V2_I1: [i128; 12] = [ - 841_344_746_069, 120_985_362_259, -30_246_340_579, -9, - 1_260_264_311, -126_026_344, -31_506_998, 6_001_018, - 469_437, -171_562, -2_238, 3_366, -]; -// Piece 2: [1.5, 2.25] — max 3 ULP -pub const POLY_V2_I2: [i128; 12] = [ - 969_603_638_235, 25_794_853_434, -9_068_503_163, 1_520_863_556, - -54_796_234, -24_375_030, 3_883_819, 18_049, - -60_032, 4_327, 413, -29, -]; -// Piece 3: [2.25, 3.0] — max 4 ULP -pub const POLY_V2_I3: [i128; 12] = [ - 995_667_551_638, 4_771_568_097, -2_348_506_182, 658_769_951, - -107_075_994, 7_184_753, 828_766, -237_082, - 16_850, 1_684, -422, -92, -]; -// Piece 4: [3.0, 4.0] — max 3 ULP -pub const POLY_V2_I4: [i128; 12] = [ - 999_767_370_921, 436_341_347, -381_798_695, 204_535_012, - -73_575_654, 18_081_420, -2_821_622, 167_292, - 39_482, -11_790, 931, 114, -]; -// Piece 5: [4.0, 5.0] — max 4 ULP -pub const POLY_V2_I5: [i128; 12] = [ - 999_996_602_327, 7_991_870, -8_990_869, 6_410_140, - -3_230_973, 1_213_662, -347_720, 75_398, - -11_582, 1_318, -130, -93, -]; - // ============================================================ // V2 HP CDF: 6 polynomial pieces + Mills ratio tail (x >= 5) // Rounding Horner (fp_mul_hp_i already rounds), boundary-constrained, @@ -659,41 +742,117 @@ pub const POLY_HP_V2_I3B_HW: i128 = 500_000_000_000_000; // I0: [0.0,0.5] deg=13 max=4 ULP pub const POLY_HP_V2_I0: [i128; 14] = [ - 598706325682924, 96667029200713, -3020844662519, -944013957034, - 46217349936, 8272413929, -471315375, -57342341, - 3603759, 323049, -21723, -1210, 4, -100, + 598706325682924, + 96667029200713, + -3020844662519, + -944013957034, + 46217349936, + 8272413929, + -471315375, + -57342341, + 3603759, + 323049, + -21723, + -1210, + 4, + -100, ]; // I1: [0.5,1.5] deg=13 max=4 ULP pub const POLY_HP_V2_I1: [i128; 14] = [ - 841344746068544, 120985362259572, -30246340565023, -33, - 1260264191835, -126026418616, -31506612696, 6001256127, - 468867241, -171906606, -1847116, 3593915, -100208, -55795, + 841344746068544, + 120985362259572, + -30246340565023, + -33, + 1260264191835, + -126026418616, + -31506612696, + 6001256127, + 468867241, + -171906606, + -1847116, + 3593915, + -100208, + -55795, ]; // I2A: [1.5,2.25] deg=15 max=4 ULP pub const POLY_HP_V2_I2A: [i128; 16] = [ - 969603638234739, 25794853435008, -9068503160759, 1520863550899, - -54796253049, -24374992132, 3883873087, 17940493, - -60092044, 4454506, 433779, -86366, 3277, 5989, -982, -1491, + 969603638234739, + 25794853435008, + -9068503160759, + 1520863550899, + -54796253049, + -24374992132, + 3883873087, + 17940493, + -60092044, + 4454506, + 433779, + -86366, + 3277, + 5989, + -982, + -1491, ]; // I2B: [2.25,3.0] deg=15 max=5 ULP pub const POLY_HP_V2_I2B: [i128; 16] = [ - 995667551636987, 4771568098811, -2348506173644, 658769960927, - -107076056464, 7184669478, 828940223, -236847082, - 16656613, 1411389, -351615, 22524, 5391, -5946, -829, 1606, + 995667551636987, + 4771568098811, + -2348506173644, + 658769960927, + -107076056464, + 7184669478, + 828940223, + -236847082, + 16656613, + 1411389, + -351615, + 22524, + 5391, + -5946, + -829, + 1606, ]; // I3A: [3.0,4.0] deg=17 max=5 ULP pub const POLY_HP_V2_I3A: [i128; 18] = [ - 999767370920965, 436341347522, -381798679096, 204535006650, - -73575786827, 18081462775, -2821236035, 167169387, - 39009455, -11645489, 1152149, 49566, -6912, 5795, - -13949, -1724, 3518, 416, + 999767370920965, + 436341347522, + -381798679096, + 204535006650, + -73575786827, + 18081462775, + -2821236035, + 167169387, + 39009455, + -11645489, + 1152149, + 49566, + -6912, + 5795, + -13949, + -1724, + 3518, + 416, ]; // I3B: [4.0,5.0] deg=17 max=5 ULP pub const POLY_HP_V2_I3B: [i128; 18] = [ - 999996602326874, 7991870552, -8990854373, 6410146188, - -3231088291, 1213608926, -347400516, 75547720, - -11941734, 1139090, 6868, -3204, 4445, -25983, - 39, 15785, -15, -3944, + 999996602326874, + 7991870552, + -8990854373, + 6410146188, + -3231088291, + 1213608926, + -347400516, + 75547720, + -11941734, + 1139090, + 6868, + -3204, + 4445, + -25983, + 39, + 15785, + -15, + -3944, ]; // ---- Derivative coefficients for norm_pdf_poly ---- @@ -975,15 +1134,15 @@ pub struct BsFull { // Fitted by scripts/remez_univariate_iv.py (quick grid, 200 DE iterations) // Max error: 0.102 σ√T on 9609-point grid pub const SQRT_2PI_IV: i128 = 2_506_628_274_631; // √(2π) × SCALE -pub const PADE_P0: i128 = 1_030_712_890_981; +pub const PADE_P0: i128 = 1_030_712_890_981; pub const PADE_P1: i128 = -3_523_716_038_286; -pub const PADE_P2: i128 = 2_286_295_838_170; -pub const PADE_P3: i128 = 148_646_789_634; -pub const PADE_P4: i128 = 72_825_165_192; +pub const PADE_P2: i128 = 2_286_295_838_170; +pub const PADE_P3: i128 = 148_646_789_634; +pub const PADE_P4: i128 = 72_825_165_192; pub const PADE_Q1: i128 = -2_282_582_703_163; -pub const PADE_Q2: i128 = -135_934_203_446; -pub const PADE_Q3: i128 = -77_424_332_735; -pub const PADE_Q4: i128 = 446_572_216; +pub const PADE_Q2: i128 = -135_934_203_446; +pub const PADE_Q3: i128 = -77_424_332_735; +pub const PADE_Q4: i128 = 446_572_216; // ============================================================ // Li rational polynomial constants for implied volatility @@ -1033,13 +1192,6 @@ pub const LI_M: [i128; 14] = [ 13_723_711_519_422, // m14 ]; -// ============================================================ -// NIG Option Pricing via COS method (Fang & Oosterlee 2008) -// ============================================================ - -pub const NIG_COS_N: usize = 17; // cosine expansion terms -pub const NIG_COS_L: i128 = 6_750_000_000_000; // truncation L=6.75 std devs (SCALE units) - // ============================================================ // AS241 inverse normal CDF coefficients (all scaled by 1e12) // Reference: Applied Statistics algorithm AS241 (1988) @@ -1055,124 +1207,124 @@ pub const SQRT_PI_OVER_TWO: i128 = 1_253_314_137_316; pub const TWO_PI_SCALED: i128 = 6_283_185_307_180; pub const SQRT_THREE_SCALED: i128 = 1_732_050_807_569; -pub const AS241_SPLIT1: i128 = 425_000_000_000; // 0.425 -pub const AS241_CONST1: i128 = 180_625_000_000; // 0.180625 -pub const AS241_SPLIT2: i128 = 5_000_000_000_000; // 5.0 -pub const AS241_CONST2: i128 = 1_600_000_000_000; // 1.6 +pub const AS241_SPLIT1: i128 = 425_000_000_000; // 0.425 +pub const AS241_CONST1: i128 = 180_625_000_000; // 0.180625 +pub const AS241_SPLIT2: i128 = 5_000_000_000_000; // 5.0 +pub const AS241_CONST2: i128 = 1_600_000_000_000; // 1.6 // Branch 1 numerator: P close to 0.5 pub const AS241_A: [i128; 8] = [ - 3_387_132_872_796, // A0 - 133_141_667_891_784, // A1 - 1_971_590_950_306_551, // A2 - 13_731_693_765_509_461, // A3 - 45_921_953_931_549_871, // A4 - 67_265_770_927_008_701, // A5 - 33_430_575_583_588_128, // A6 - 2_509_080_928_730_123, // A7 + 3_387_132_872_796, // A0 + 133_141_667_891_784, // A1 + 1_971_590_950_306_551, // A2 + 13_731_693_765_509_461, // A3 + 45_921_953_931_549_871, // A4 + 67_265_770_927_008_701, // A5 + 33_430_575_583_588_128, // A6 + 2_509_080_928_730_123, // A7 ]; // Branch 1 denominator pub const AS241_B: [i128; 7] = [ - 42_313_330_701_601, // B1 - 687_187_007_492_058, // B2 - 5_394_196_021_424_751, // B3 - 21_213_794_301_586_596, // B4 - 39_307_895_800_092_711, // B5 - 28_729_085_735_721_943, // B6 - 5_226_495_278_852_855, // B7 + 42_313_330_701_601, // B1 + 687_187_007_492_058, // B2 + 5_394_196_021_424_751, // B3 + 21_213_794_301_586_596, // B4 + 39_307_895_800_092_711, // B5 + 28_729_085_735_721_943, // B6 + 5_226_495_278_852_855, // B7 ]; // Branch 2 numerator: P not close to 0, 0.5, or 1 pub const AS241_C: [i128; 8] = [ - 1_423_437_110_750, // C0 - 4_630_337_846_157, // C1 - 5_769_497_221_461, // C2 - 3_647_848_324_763, // C3 - 1_270_458_252_452, // C4 - 241_780_725_177, // C5 - 22_723_844_989, // C6 - 774_545_014, // C7 + 1_423_437_110_750, // C0 + 4_630_337_846_157, // C1 + 5_769_497_221_461, // C2 + 3_647_848_324_763, // C3 + 1_270_458_252_452, // C4 + 241_780_725_177, // C5 + 22_723_844_989, // C6 + 774_545_014, // C7 ]; // Branch 2 denominator pub const AS241_D: [i128; 7] = [ - 2_053_191_626_638, // D1 - 1_676_384_830_184, // D2 - 689_767_334_985, // D3 - 148_103_976_427, // D4 - 15_198_666_564, // D5 - 547_593_808, // D6 - 1_051, // D7 + 2_053_191_626_638, // D1 + 1_676_384_830_184, // D2 + 689_767_334_985, // D3 + 148_103_976_427, // D4 + 15_198_666_564, // D5 + 547_593_808, // D6 + 1_051, // D7 ]; // Branch 3 numerator: P very close to 0 or 1 pub const AS241_E: [i128; 8] = [ - 6_657_904_643_501, // E0 - 5_463_784_911_164, // E1 - 1_784_826_539_917, // E2 - 296_560_571_829, // E3 - 26_532_189_527, // E4 - 1_242_660_947, // E5 - 27_115_556, // E6 - 201_033, // E7 + 6_657_904_643_501, // E0 + 5_463_784_911_164, // E1 + 1_784_826_539_917, // E2 + 296_560_571_829, // E3 + 26_532_189_527, // E4 + 1_242_660_947, // E5 + 27_115_556, // E6 + 201_033, // E7 ]; // Branch 3 denominator pub const AS241_F: [i128; 7] = [ - 599_832_206_556, // F1 - 136_929_880_923, // F2 - 14_875_361_291, // F3 - 786_869_131, // F4 - 18_463_183, // F5 - 142_151, // F6 - 0, // F7 (2.04e-15, rounds to 0) + 599_832_206_556, // F1 + 136_929_880_923, // F2 + 14_875_361_291, // F3 + 786_869_131, // F4 + 18_463_183, // F5 + 142_151, // F6 + 0, // F7 (2.04e-15, rounds to 0) ]; // AS241 Branch 2b/2c split points and coefficients (5-branch inverse CDF). -pub const AS241_SPLIT_2B: i128 = 3_000_000_000_000; // 3.0 -pub const AS241_CENTER_2B: i128 = 3_500_000_000_000; // 3.5 -pub const AS241_SPLIT_2C: i128 = 4_000_000_000_000; // 4.0 -pub const AS241_CENTER_2C: i128 = 4_500_000_000_000; // 4.5 +pub const AS241_SPLIT_2B: i128 = 3_000_000_000_000; // 3.0 +pub const AS241_CENTER_2B: i128 = 3_500_000_000_000; // 3.5 +pub const AS241_SPLIT_2C: i128 = 4_000_000_000_000; // 4.0 +pub const AS241_CENTER_2C: i128 = 4_500_000_000_000; // 4.5 pub const AS241_G: [i128; 8] = [ 4_426_662_374_924, - 88_005_498_751, - -41_160_164_851, - 39_126_451_267, - -58_494_244_713, - 66_910_003_962, - 33_959_185_442, - 3_528_770_658, + 88_005_498_751, + -41_160_164_851, + 39_126_451_267, + -58_494_244_713, + 66_910_003_962, + 33_959_185_442, + 3_528_770_658, ]; pub const AS241_H: [i128; 7] = [ - -321_373_526_953, - 105_028_553_616, - -29_555_849_092, - -2_051_462_415, - 15_427_578_835, - 2_488_089_624, - 293_654, + -321_373_526_953, + 105_028_553_616, + -29_555_849_092, + -2_051_462_415, + 15_427_578_835, + 2_488_089_624, + 293_654, ]; pub const AS241_I: [i128; 8] = [ 5_920_458_342_163, - 44_499_785_186, - -13_437_857_419, - 10_089_893_956, - -18_863_036_526, - 29_961_578_652, - 10_532_292_799, - 830_578_919, + 44_499_785_186, + -13_437_857_419, + 10_089_893_956, + -18_863_036_526, + 29_961_578_652, + 10_532_292_799, + 830_578_919, ]; pub const AS241_J: [i128; 7] = [ - -242_472_999_986, - 60_245_461_302, - -14_160_150_896, - 615_344_034, - 4_831_648_864, - 586_268_402, - 34_994, + -242_472_999_986, + 60_245_461_302, + -14_160_150_896, + 615_344_034, + 4_831_648_864, + 586_268_402, + 34_994, ]; /// Step size for 16-entry ln table: SCALE / 16. @@ -1189,85 +1341,85 @@ pub const LN_TABLE_HP_HALF_STEP: u128 = SCALE_HP_U / 32; /// Entry j = round(ln(1 + (2j+1)/32) × SCALE). /// Midpoint of interval j is SCALE + (2j+1) × SCALE/32. pub const LN_TABLE_16: [i128; 16] = [ - 30771658667, // j= 0: ln(1.03125) - 89612158690, // j= 1: ln(1.09375) - 145182009844, // j= 2: ln(1.15625) - 197825743330, // j= 3: ln(1.21875) - 247836163905, // j= 4: ln(1.28125) - 295464212894, // j= 5: ln(1.34375) - 340926586971, // j= 6: ln(1.40625) - 384411698910, // j= 7: ln(1.46875) - 426084395311, // j= 8: ln(1.53125) - 466089729925, // j= 9: ln(1.59375) - 504556010752, // j=10: ln(1.65625) - 541597282433, // j=11: ln(1.71875) - 577315365035, // j=12: ln(1.78125) - 611801541106, // j=13: ln(1.84375) - 645137961374, // j=14: ln(1.90625) - 677398823592, // j=15: ln(1.96875) + 30771658667, // j= 0: ln(1.03125) + 89612158690, // j= 1: ln(1.09375) + 145182009844, // j= 2: ln(1.15625) + 197825743330, // j= 3: ln(1.21875) + 247836163905, // j= 4: ln(1.28125) + 295464212894, // j= 5: ln(1.34375) + 340926586971, // j= 6: ln(1.40625) + 384411698910, // j= 7: ln(1.46875) + 426084395311, // j= 8: ln(1.53125) + 466089729925, // j= 9: ln(1.59375) + 504556010752, // j=10: ln(1.65625) + 541597282433, // j=11: ln(1.71875) + 577315365035, // j=12: ln(1.78125) + 611801541106, // j=13: ln(1.84375) + 645137961374, // j=14: ln(1.90625) + 677398823592, // j=15: ln(1.96875) ]; /// Sub-ULP residuals for LN_TABLE_16. /// true_ln(midpoint) × SCALE = LN_TABLE_16[j] + LN_TABLE_LO_16[j] / SCALE. /// |LN_TABLE_LO_16[j]| < SCALE. Generated with mpmath at 60 decimal digits. pub const LN_TABLE_LO_16: [i128; 16] = [ - -246311628972, // j= 0 - -312867380049, // j= 1 - 497897281935, // j= 2 - -80119637428, // j= 3 - -418743219397, // j= 4 - -164123613318, // j= 5 - -406789694911, // j= 6 - 332039734790, // j= 7 - -99936875455, // j= 8 - -400775441381, // j= 9 - 395287058309, // j=10 - -255628423458, // j=11 - -176395681888, // j=12 - -7096470110, // j=13 - -415298334772, // j=14 - -193859190317, // j=15 + -246311628972, // j= 0 + -312867380049, // j= 1 + 497897281935, // j= 2 + -80119637428, // j= 3 + -418743219397, // j= 4 + -164123613318, // j= 5 + -406789694911, // j= 6 + 332039734790, // j= 7 + -99936875455, // j= 8 + -400775441381, // j= 9 + 395287058309, // j=10 + -255628423458, // j=11 + -176395681888, // j=12 + -7096470110, // j=13 + -415298334772, // j=14 + -193859190317, // j=15 ]; /// HP 16-entry lookup table for table-assisted ln at SCALE_HP = 1e15. pub const LN_TABLE_HP_16: [i128; 16] = [ - 30771658666754, // j= 0: ln(1.03125) - 89612158689687, // j= 1: ln(1.09375) - 145182009844498, // j= 2: ln(1.15625) - 197825743329920, // j= 3: ln(1.21875) - 247836163904581, // j= 4: ln(1.28125) - 295464212893836, // j= 5: ln(1.34375) - 340926586970593, // j= 6: ln(1.40625) - 384411698910332, // j= 7: ln(1.46875) - 426084395310900, // j= 8: ln(1.53125) - 466089729924599, // j= 9: ln(1.59375) - 504556010752395, // j=10: ln(1.65625) - 541597282432744, // j=11: ln(1.71875) - 577315365034824, // j=12: ln(1.78125) - 611801541105993, // j=13: ln(1.84375) - 645137961373585, // j=14: ln(1.90625) - 677398823591806, // j=15: ln(1.96875) + 30771658666754, // j= 0: ln(1.03125) + 89612158689687, // j= 1: ln(1.09375) + 145182009844498, // j= 2: ln(1.15625) + 197825743329920, // j= 3: ln(1.21875) + 247836163904581, // j= 4: ln(1.28125) + 295464212893836, // j= 5: ln(1.34375) + 340926586970593, // j= 6: ln(1.40625) + 384411698910332, // j= 7: ln(1.46875) + 426084395310900, // j= 8: ln(1.53125) + 466089729924599, // j= 9: ln(1.59375) + 504556010752395, // j=10: ln(1.65625) + 541597282432744, // j=11: ln(1.71875) + 577315365034824, // j=12: ln(1.78125) + 611801541105993, // j=13: ln(1.84375) + 645137961373585, // j=14: ln(1.90625) + 677398823591806, // j=15: ln(1.96875) ]; /// Sub-ULP residuals for LN_TABLE_HP_16. /// true_ln(midpoint) × SCALE_HP = LN_TABLE_HP_16[j] + LN_TABLE_HP_LO_16[j] / SCALE_HP. pub const LN_TABLE_HP_LO_16: [i128; 16] = [ - -311628971792403, // j= 0 - 132619951469378, // j= 1 - -102718064936259, // j= 2 - -119637427928803, // j= 3 - 256780602765747, // j= 4 - -123613318093945, // j= 5 - 210305089199780, // j= 6 - 39734790062481, // j= 7 - 63124544879595, // j= 8 - 224558619247505, // j= 9 - 287058308531738, // j=10 - 371576542303900, // j=11 - -395681887938481, // j=12 - -96470110233571, // j=13 - -298334771503865, // j=14 - 140809682609997, // j=15 + -311628971792403, // j= 0 + 132619951469378, // j= 1 + -102718064936259, // j= 2 + -119637427928803, // j= 3 + 256780602765747, // j= 4 + -123613318093945, // j= 5 + 210305089199780, // j= 6 + 39734790062481, // j= 7 + 63124544879595, // j= 8 + 224558619247505, // j= 9 + 287058308531738, // j=10 + 371576542303900, // j=11 + -395681887938481, // j=12 + -96470110233571, // j=13 + -298334771503865, // j=14 + 140809682609997, // j=15 ]; #[cfg(test)] @@ -1276,14 +1428,20 @@ mod tests { #[test] fn test_ln2_lo_sub_ulp() { - assert!((LN2_LO as u128) < SCALE || ((-LN2_LO) as u128) < SCALE, - "LN2_LO={} not sub-ULP", LN2_LO); + assert!( + (LN2_LO as u128) < SCALE || ((-LN2_LO) as u128) < SCALE, + "LN2_LO={} not sub-ULP", + LN2_LO + ); } #[test] fn test_ln2_hp_lo_sub_ulp() { - assert!((LN2_HP_LO.unsigned_abs()) < SCALE_HP_U, - "LN2_HP_LO={} not sub-ULP", LN2_HP_LO); + assert!( + (LN2_HP_LO.unsigned_abs()) < SCALE_HP_U, + "LN2_HP_LO={} not sub-ULP", + LN2_HP_LO + ); } #[test] @@ -1309,8 +1467,12 @@ mod tests { } else { (raw - SCALE_I / 2) / SCALE_I }; - assert!(correction.abs() <= 10, - "Correction too large at k={}: {}", k, correction); + assert!( + correction.abs() <= 10, + "Correction too large at k={}: {}", + k, + correction + ); } } @@ -1330,26 +1492,40 @@ mod tests { #[test] fn test_ln_table_lo_sub_ulp() { for j in 0..16 { - assert!(LN_TABLE_LO_16[j].unsigned_abs() < SCALE, - "LN_TABLE_LO_16[{}]={} not sub-ULP", j, LN_TABLE_LO_16[j]); + assert!( + LN_TABLE_LO_16[j].unsigned_abs() < SCALE, + "LN_TABLE_LO_16[{}]={} not sub-ULP", + j, + LN_TABLE_LO_16[j] + ); } } #[test] fn test_ln_table_hp_lo_sub_ulp() { for j in 0..16 { - assert!(LN_TABLE_HP_LO_16[j].unsigned_abs() < SCALE_HP_U, - "LN_TABLE_HP_LO_16[{}]={} not sub-ULP", j, LN_TABLE_HP_LO_16[j]); + assert!( + LN_TABLE_HP_LO_16[j].unsigned_abs() < SCALE_HP_U, + "LN_TABLE_HP_LO_16[{}]={} not sub-ULP", + j, + LN_TABLE_HP_LO_16[j] + ); } } #[test] fn test_ln_table_monotone() { for j in 1..16 { - assert!(LN_TABLE_16[j] > LN_TABLE_16[j-1], - "LN_TABLE_16 not monotone at j={}", j); - assert!(LN_TABLE_HP_16[j] > LN_TABLE_HP_16[j-1], - "LN_TABLE_HP_16 not monotone at j={}", j); + assert!( + LN_TABLE_16[j] > LN_TABLE_16[j - 1], + "LN_TABLE_16 not monotone at j={}", + j + ); + assert!( + LN_TABLE_HP_16[j] > LN_TABLE_HP_16[j - 1], + "LN_TABLE_HP_16 not monotone at j={}", + j + ); } } } diff --git a/src/double_word.rs b/src/double_word.rs index fa00765..38aec41 100644 --- a/src/double_word.rs +++ b/src/double_word.rs @@ -39,10 +39,20 @@ impl DoubleWord { #[inline] pub(crate) fn to_i128_at_scale(self, scale: i128) -> i128 { let half = scale / 2; - let correction = if self.lo >= 0 { - (self.lo + half) / scale + let abs_lo = self.lo.unsigned_abs(); + let correction = if abs_lo < half as u128 { + 0 + } else if abs_lo > half as u128 { + self.lo.signum() + } else if self.lo > 0 && self.hi >= 0 { + 1 + } else if self.lo < 0 && self.hi <= 0 { + -1 } else { - (self.lo - half) / scale + // `hi` was already rounded away from zero and the opposite-sign + // half-ULP residual records the exact tie. Applying another + // correction here would undo the original rounding. + 0 }; self.hi + correction } @@ -51,7 +61,7 @@ impl DoubleWord { /// Carries overflow from lo into hi. Returns Err on hi overflow. #[allow(dead_code)] #[inline] - pub(crate) fn add(self, other: Self) -> Result { + pub(crate) fn checked_add(self, other: Self) -> Result { let lo_sum = self.lo + other.lo; let carry = if lo_sum >= SCALE_I { 1 @@ -60,8 +70,23 @@ impl DoubleWord { } else { 0 }; - let hi = self.hi.checked_add(other.hi) + // Try all safe association orders. The first pair can overflow even + // when the carry cancels it and the exact three-term sum is in range. + let hi = self + .hi + .checked_add(other.hi) .and_then(|h| h.checked_add(carry)) + .or_else(|| { + self.hi + .checked_add(carry) + .and_then(|h| h.checked_add(other.hi)) + }) + .or_else(|| { + other + .hi + .checked_add(carry) + .and_then(|h| h.checked_add(self.hi)) + }) .ok_or(crate::error::SolMathError::Overflow)?; Ok(Self { hi, @@ -71,17 +96,91 @@ impl DoubleWord { #[allow(dead_code)] #[inline] - pub(crate) const fn hi(self) -> i128 { + pub const fn hi(self) -> i128 { self.hi } #[allow(dead_code)] #[inline] - pub(crate) const fn lo(self) -> i128 { + pub const fn lo(self) -> i128 { self.lo } } +#[cfg(kani)] +mod verification { + use super::*; + use crate::constants::SCALE_HP; + + fn prove_collapse_is_half_ulp(scale: i128) { + let hi: i128 = kani::any(); + let lo: i128 = kani::any(); + kani::assume(lo > -scale); + kani::assume(lo < scale); + + let half = scale / 2; + let magnitude = lo.unsigned_abs(); + let correction = if magnitude < half as u128 { + 0 + } else if magnitude > half as u128 { + lo.signum() + } else if lo > 0 && hi >= 0 { + 1 + } else if lo < 0 && hi <= 0 { + -1 + } else { + 0 + }; + let expected = hi.checked_add(correction); + kani::assume(expected.is_some()); + + let actual = DoubleWord::new_raw(hi, lo).to_i128_at_scale(scale); + let error_numerator = if correction == 0 { + magnitude + } else { + scale as u128 - magnitude + }; + + assert_eq!(actual, expected.unwrap()); + assert!(error_numerator <= half as u128); + } + + /// Prove collapsing every valid standard-scale residual rounds to nearest + /// with ties away from zero and at most one-half output ULP of error. + #[kani::proof] + fn standard_collapse_is_half_ulp_for_every_valid_residual() { + prove_collapse_is_half_ulp(SCALE_I); + } + + /// Prove the same collapse property at the crate's high-precision scale. + #[kani::proof] + fn hp_collapse_is_half_ulp_for_every_valid_residual() { + prove_collapse_is_half_ulp(SCALE_HP); + } + + /// Prove exact double-word addition re-normalizes every pair of valid + /// sub-ULP residuals back into `(-SCALE, SCALE)` whenever the high word is + /// representable. This preserves the invariant required by later ULP + /// collapse proofs. + #[kani::proof] + fn checked_add_preserves_the_sub_ulp_residual_invariant() { + let a_hi: i128 = kani::any(); + let a_lo: i128 = kani::any(); + let b_hi: i128 = kani::any(); + let b_lo: i128 = kani::any(); + kani::assume(a_lo > -SCALE_I); + kani::assume(a_lo < SCALE_I); + kani::assume(b_lo > -SCALE_I); + kani::assume(b_lo < SCALE_I); + + let a = DoubleWord::new_raw(a_hi, a_lo); + let b = DoubleWord::new_raw(b_hi, b_lo); + if let Ok(sum) = a.checked_add(b) { + assert!(sum.lo.unsigned_abs() < SCALE_I as u128); + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -96,53 +195,83 @@ mod tests { #[test] fn test_dw_to_i128_no_correction() { - let dw = DoubleWord { hi: 5 * SCALE_I, lo: 0 }; + let dw = DoubleWord { + hi: 5 * SCALE_I, + lo: 0, + }; assert_eq!(dw.to_i128(), 5 * SCALE_I); } #[test] fn test_dw_to_i128_positive_correction() { // lo >= SCALE/2 should round hi up by 1 - let dw = DoubleWord { hi: 5 * SCALE_I, lo: SCALE_I / 2 }; + let dw = DoubleWord { + hi: 5 * SCALE_I, + lo: SCALE_I / 2, + }; assert_eq!(dw.to_i128(), 5 * SCALE_I + 1); } #[test] fn test_dw_to_i128_negative_correction() { - let dw = DoubleWord { hi: 5 * SCALE_I, lo: -SCALE_I / 2 }; - assert_eq!(dw.to_i128(), 5 * SCALE_I - 1); + let dw = DoubleWord { + hi: 5 * SCALE_I, + lo: -SCALE_I / 2, + }; + assert_eq!(dw.to_i128(), 5 * SCALE_I); } #[test] fn test_dw_to_i128_small_lo_no_correction() { // lo < SCALE/2 should not change hi - let dw = DoubleWord { hi: 5 * SCALE_I, lo: SCALE_I / 2 - 1 }; + let dw = DoubleWord { + hi: 5 * SCALE_I, + lo: SCALE_I / 2 - 1, + }; assert_eq!(dw.to_i128(), 5 * SCALE_I); } #[test] fn test_dw_add_simple() { - let a = DoubleWord { hi: 3 * SCALE_I, lo: 100 }; - let b = DoubleWord { hi: 4 * SCALE_I, lo: 200 }; - let c = a.add(b).unwrap(); + let a = DoubleWord { + hi: 3 * SCALE_I, + lo: 100, + }; + let b = DoubleWord { + hi: 4 * SCALE_I, + lo: 200, + }; + let c = a.checked_add(b).unwrap(); assert_eq!(c.hi, 7 * SCALE_I); assert_eq!(c.lo, 300); } #[test] fn test_dw_add_lo_carry() { - let a = DoubleWord { hi: 3 * SCALE_I, lo: SCALE_I - 100 }; - let b = DoubleWord { hi: 4 * SCALE_I, lo: 200 }; - let c = a.add(b).unwrap(); + let a = DoubleWord { + hi: 3 * SCALE_I, + lo: SCALE_I - 100, + }; + let b = DoubleWord { + hi: 4 * SCALE_I, + lo: 200, + }; + let c = a.checked_add(b).unwrap(); assert_eq!(c.hi, 7 * SCALE_I + 1); assert_eq!(c.lo, 100); } #[test] fn test_dw_add_lo_negative_carry() { - let a = DoubleWord { hi: 3 * SCALE_I, lo: -(SCALE_I - 100) }; - let b = DoubleWord { hi: 4 * SCALE_I, lo: -200 }; - let c = a.add(b).unwrap(); + let a = DoubleWord { + hi: 3 * SCALE_I, + lo: -(SCALE_I - 100), + }; + let b = DoubleWord { + hi: 4 * SCALE_I, + lo: -200, + }; + let c = a.checked_add(b).unwrap(); assert_eq!(c.hi, 7 * SCALE_I - 1); assert_eq!(c.lo, -100); } @@ -152,16 +281,33 @@ mod tests { // After add, |lo| < SCALE must hold // Use hi values that won't overflow when summed with carry let extremes = [ - DoubleWord { hi: 0, lo: SCALE_I - 1 }, - DoubleWord { hi: 0, lo: -(SCALE_I - 1) }, - DoubleWord { hi: 1_000 * SCALE_I, lo: SCALE_I - 1 }, - DoubleWord { hi: -1_000 * SCALE_I, lo: -(SCALE_I - 1) }, + DoubleWord { + hi: 0, + lo: SCALE_I - 1, + }, + DoubleWord { + hi: 0, + lo: -(SCALE_I - 1), + }, + DoubleWord { + hi: 1_000 * SCALE_I, + lo: SCALE_I - 1, + }, + DoubleWord { + hi: -1_000 * SCALE_I, + lo: -(SCALE_I - 1), + }, ]; for &a in &extremes { for &b in &extremes { - let c = a.add(b).unwrap(); - assert!(c.lo.abs() < SCALE_I, - "lo invariant violated: a={:?}, b={:?}, result={:?}", a, b, c); + let c = a.checked_add(b).unwrap(); + assert!( + c.lo.abs() < SCALE_I, + "lo invariant violated: a={:?}, b={:?}, result={:?}", + a, + b, + c + ); } } } @@ -169,7 +315,15 @@ mod tests { #[test] fn test_dw_to_i128_roundtrip() { // from_hi(x).to_i128() == x for any x - for x in [0i128, 1, -1, SCALE_I, -SCALE_I, i128::MAX / 2, i128::MIN / 2] { + for x in [ + 0i128, + 1, + -1, + SCALE_I, + -SCALE_I, + i128::MAX / 2, + i128::MIN / 2, + ] { assert_eq!(DoubleWord::from_hi(x).to_i128(), x); } } @@ -179,17 +333,39 @@ mod tests { #[test] fn test_dw_at_scale_matches_to_i128() { let cases = [ - DoubleWord { hi: 5 * SCALE_I, lo: 0 }, - DoubleWord { hi: 5 * SCALE_I, lo: SCALE_I / 2 }, - DoubleWord { hi: 5 * SCALE_I, lo: -SCALE_I / 2 }, - DoubleWord { hi: 5 * SCALE_I, lo: SCALE_I / 2 - 1 }, + DoubleWord { + hi: 5 * SCALE_I, + lo: 0, + }, + DoubleWord { + hi: 5 * SCALE_I, + lo: SCALE_I / 2, + }, + DoubleWord { + hi: 5 * SCALE_I, + lo: -SCALE_I / 2, + }, + DoubleWord { + hi: 5 * SCALE_I, + lo: SCALE_I / 2 - 1, + }, DoubleWord { hi: 0, lo: 0 }, - DoubleWord { hi: -3 * SCALE_I, lo: 100 }, - DoubleWord { hi: -3 * SCALE_I, lo: -100 }, + DoubleWord { + hi: -3 * SCALE_I, + lo: 100, + }, + DoubleWord { + hi: -3 * SCALE_I, + lo: -100, + }, ]; for dw in cases { - assert_eq!(dw.to_i128(), dw.to_i128_at_scale(SCALE_I), - "Mismatch for {:?}", dw); + assert_eq!( + dw.to_i128(), + dw.to_i128_at_scale(SCALE_I), + "Mismatch for {:?}", + dw + ); } } @@ -197,11 +373,17 @@ mod tests { fn test_dw_at_scale_hp() { use crate::constants::SCALE_HP; // lo = SCALE_HP/2 should round up by 1 - let dw = DoubleWord { hi: 5 * SCALE_HP, lo: SCALE_HP / 2 }; + let dw = DoubleWord { + hi: 5 * SCALE_HP, + lo: SCALE_HP / 2, + }; assert_eq!(dw.to_i128_at_scale(SCALE_HP), 5 * SCALE_HP + 1); // lo = SCALE_HP/2 - 1 should not round - let dw2 = DoubleWord { hi: 5 * SCALE_HP, lo: SCALE_HP / 2 - 1 }; + let dw2 = DoubleWord { + hi: 5 * SCALE_HP, + lo: SCALE_HP / 2 - 1, + }; assert_eq!(dw2.to_i128_at_scale(SCALE_HP), 5 * SCALE_HP); } @@ -209,10 +391,16 @@ mod tests { fn test_dw_at_scale_hp_negative() { use crate::constants::SCALE_HP; // Negative lo at HP scale - let dw = DoubleWord { hi: 5 * SCALE_HP, lo: -SCALE_HP / 2 }; - assert_eq!(dw.to_i128_at_scale(SCALE_HP), 5 * SCALE_HP - 1); + let dw = DoubleWord { + hi: 5 * SCALE_HP, + lo: -SCALE_HP / 2, + }; + assert_eq!(dw.to_i128_at_scale(SCALE_HP), 5 * SCALE_HP); - let dw2 = DoubleWord { hi: 5 * SCALE_HP, lo: -(SCALE_HP / 2 - 1) }; + let dw2 = DoubleWord { + hi: 5 * SCALE_HP, + lo: -(SCALE_HP / 2 - 1), + }; assert_eq!(dw2.to_i128_at_scale(SCALE_HP), 5 * SCALE_HP); } @@ -223,4 +411,25 @@ mod tests { assert_eq!(DoubleWord::from_hi(x).to_i128_at_scale(SCALE_HP), x); } } + + #[test] + fn test_dw_exact_half_ties_round_away_from_zero_once() { + let pos = DoubleWord::new_raw(1, -SCALE_I / 2); + let neg = DoubleWord::new_raw(-1, SCALE_I / 2); + assert_eq!(pos.to_i128(), 1); + assert_eq!(neg.to_i128(), -1); + + let pos_from_zero = DoubleWord::new_raw(0, SCALE_I / 2); + let neg_from_zero = DoubleWord::new_raw(0, -SCALE_I / 2); + assert_eq!(pos_from_zero.to_i128(), 1); + assert_eq!(neg_from_zero.to_i128(), -1); + } + + #[test] + fn test_dw_add_carry_can_cancel_intermediate_overflow() { + let a = DoubleWord::new_raw(i128::MAX, -SCALE_I / 2); + let b = DoubleWord::new_raw(1, -SCALE_I / 2); + let sum = a.checked_add(b).unwrap(); + assert_eq!(sum, DoubleWord::from_hi(i128::MAX)); + } } diff --git a/src/error.rs b/src/error.rs index 25d0651..da15809 100644 --- a/src/error.rs +++ b/src/error.rs @@ -10,3 +10,16 @@ pub enum SolMathError { /// Iterative method did not converge (e.g. implied_vol) NoConvergence, } + +impl core::fmt::Display for SolMathError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(match self { + Self::DomainError => "input outside the mathematical domain", + Self::Overflow => "result would overflow the representable range", + Self::DivisionByZero => "division by zero", + Self::NoConvergence => "iterative method did not converge", + }) + } +} + +// core::error::Error requires Rust 1.81; the crate's MSRV remains 1.79. diff --git a/src/exp_coeffs.rs b/src/exp_coeffs.rs new file mode 100644 index 0000000..9c0af12 --- /dev/null +++ b/src/exp_coeffs.rs @@ -0,0 +1,64 @@ +// @generated by scripts/generate_exp_coeffs.py; do not edit manually. + +/// Split multiplier for converting a raw SCALE residual to Q63 without a +/// wide multiply. Together these approximate `2^64 / SCALE`. +pub(crate) const EXP_RAW_TO_Q63_HI: i64 = 18_446_744; +pub(crate) const EXP_RAW_TO_Q63_FRAC_Q28: i64 = 19_786_257; + +/// `round((ln(2) - LN2_I / SCALE) * 2^96)`. +pub(crate) const EXP_LN2_RESIDUAL_Q96: i64 = -4_333_034_379_533_306; + +/// `round((ln(2) / 32) * 2^63)`. +pub(crate) const EXP_STEP_Q63: i64 = 199_786_072_581_291_495; + +pub(crate) const EXP_POLY_GUARD: i32 = 22; +pub(crate) const EXP_PHASE_BITS: i32 = 5; +pub(crate) const EXP_PHASES: usize = 1 << EXP_PHASE_BITS; + +/// Degree-5 near-minimax coefficients for exp(r) on +/// `[-ln(2)/64, ln(2)/64]`, descending, at `SCALE * 2^22`. +pub(crate) const EXP_REMEZ_Q22: [i64; 6] = [ + 34_952_728_565_492_453, + 174_763_691_635_588_304, + 699_050_666_653_069_566, + 2_097_151_999_954_914_874, + 4_194_304_000_000_000_252, + 4_194_304_000_000_000_294, +]; + +/// `round(2^(phase/32) * 2^62)`. These are reconstruction constants, +/// not sampled values of the exponential kernel. +pub(crate) const EXP2_PHASE_Q62: [i64; EXP_PHASES] = [ + 4_611_686_018_427_387_904, + 4_712_668_792_719_003_884, + 4_815_862_801_830_788_490, + 4_921_316_465_500_308_116, + 5_029_079_263_719_320_435, + 5_139_201_759_950_318_048, + 5_251_735_624_851_448_219, + 5_366_733_660_520_940_721, + 5_484_249_825_272_419_512, + 5_604_339_258_952_723_100, + 5_727_058_308_814_112_983, + 5_852_464_555_953_009_676, + 5_980_616_842_327_661_685, + 6_111_575_298_367_424_380, + 6_245_401_371_186_603_363, + 6_382_157_853_416_100_552, + 6_521_908_912_666_391_106, + 6_664_720_121_635_655_541, + 6_810_658_488_877_194_079, + 6_959_792_490_240_559_659, + 7_112_192_101_001_162_095, + 7_267_928_828_693_418_961, + 7_427_075_746_662_858_866, + 7_589_707_528_352_920_109, + 7_755_900_482_342_532_474, + 7_925_732_588_150_922_155, + 8_099_283_532_826_439_817, + 8_276_634_748_336_579_668, + 8_457_869_449_776_733_335, + 8_643_072_674_415_606_502, + 8_832_331_321_595_618_838, + 9_025_734_193_507_008_925, +]; diff --git a/src/expm1_lut.rs b/src/expm1_lut.rs new file mode 100644 index 0000000..519dee9 --- /dev/null +++ b/src/expm1_lut.rs @@ -0,0 +1,1303 @@ +// @generated by scripts/generate_expm1_lut.py; do not edit manually. + +pub(crate) const EXPM1_R_MIN: i64 = -346573590280; +pub(crate) const EXPM1_LUT_STEP_SHIFT: u32 = 29; +pub(crate) const EXPM1_LUT_STEP: i64 = 536870912; +pub(crate) const EXPM1_LUT_SEGMENTS: usize = 1292; +pub(crate) const EXPM1_INV_LN2_Q56: i64 = 103957; +pub(crate) const EXPM1_RAW_TO_Q43_G31: i64 = 18889465931; + +pub(crate) const EXPM1_MID_EXP_RAW_Q22: [i64; EXPM1_LUT_SEGMENTS] = [ + 2966617039081363113, + 2968210157087782562, + 2969804130622552635, + 2971398960145104935, + 2972994646115117785, + 2974591188992516365, + 2976188589237472838, + 2977786847310406490, + 2979385963671983857, + 2980985938783118862, + 2982586773104972942, + 2984188467098955188, + 2985791021226722473, + 2987394435950179588, + 2988998711731479372, + 2990603849033022848, + 2992209848317459356, + 2993816710047686685, + 2995424434686851206, + 2997033022698348009, + 2998642474545821032, + 3000252790693163197, + 3001863971604516545, + 3003476017744272367, + 3005088929577071340, + 3006702707567803658, + 3008317352181609170, + 3009932863883877512, + 3011549243140248241, + 3013166490416610969, + 3014784606179105497, + 3016403590894121953, + 3018023445028300920, + 3019644169048533576, + 3021265763421961826, + 3022888228615978438, + 3024511565098227177, + 3026135773336602938, + 3027760853799251884, + 3029386806954571579, + 3031013633271211125, + 3032641333218071293, + 3034269907264304663, + 3035899355879315756, + 3037529679532761170, + 3039160878694549717, + 3040792953834842557, + 3042425905424053331, + 3044059733932848302, + 3045694439832146487, + 3047330023593119794, + 3048966485687193156, + 3050603826586044670, + 3052242046761605730, + 3053881146686061165, + 3055521126831849375, + 3057161987671662464, + 3058803729678446381, + 3060446353325401054, + 3062089859085980526, + 3063734247433893092, + 3065379518843101435, + 3067025673787822763, + 3068672712742528948, + 3070320636181946658, + 3071969444581057496, + 3073619138415098140, + 3075269718159560476, + 3076921184290191735, + 3078573537282994633, + 3080226777614227505, + 3081880905760404447, + 3083535922198295447, + 3085191827404926527, + 3086848621857579879, + 3088506306033794002, + 3090164880411363843, + 3091824345468340929, + 3093484701683033509, + 3095145949534006692, + 3096808089500082580, + 3098471122060340415, + 3100135047694116707, + 3101799866881005379, + 3103465580100857904, + 3105132187833783441, + 3106799690560148975, + 3108468088760579455, + 3110137382915957935, + 3111807573507425708, + 3113478661016382448, + 3115150645924486347, + 3116823528713654257, + 3118497309866061825, + 3120171989864143633, + 3121847569190593339, + 3123524048328363814, + 3125201427760667281, + 3126879707970975458, + 3128558889443019692, + 3130238972660791103, + 3131919958108540718, + 3133601846270779620, + 3135284637632279075, + 3136968332678070684, + 3138652931893446514, + 3140338435763959242, + 3142024844775422295, + 3143712159413909988, + 3145400380165757666, + 3147089507517561842, + 3148779541956180341, + 3150470483968732436, + 3152162334042598991, + 3153855092665422601, + 3155548760325107733, + 3157243337509820864, + 3158938824707990625, + 3160635222408307942, + 3162332531099726174, + 3164030751271461254, + 3165729883412991833, + 3167429928014059420, + 3169130885564668521, + 3170832756555086782, + 3172535541475845131, + 3174239240817737918, + 3175943855071823057, + 3177649384729422167, + 3179355830282120715, + 3181063192221768157, + 3182771471040478077, + 3184480667230628335, + 3186190781284861203, + 3187901813696083510, + 3189613764957466784, + 3191326635562447392, + 3193040426004726684, + 3194755136778271138, + 3196470768377312494, + 3198187321296347908, + 3199904796030140084, + 3201623193073717422, + 3203342512922374161, + 3205062756071670520, + 3206783923017432839, + 3208506014255753728, + 3210229030282992204, + 3211952971595773836, + 3213677838690990889, + 3215403632065802467, + 3217130352217634655, + 3218857999644180665, + 3220586574843400975, + 3222316078313523477, + 3224046510553043620, + 3225777872060724551, + 3227510163335597260, + 3229243384876960725, + 3230977537184382054, + 3232712620757696632, + 3234448636097008262, + 3236185583702689310, + 3237923464075380850, + 3239662277715992806, + 3241402025125704102, + 3243142706805962799, + 3244884323258486245, + 3246626874985261217, + 3248370362488544067, + 3250114786270860865, + 3251860146835007548, + 3253606444684050058, + 3255353680321324494, + 3257101854250437253, + 3258850966975265177, + 3260601018999955695, + 3262352010828926973, + 3264103942966868058, + 3265856815918739019, + 3267610630189771101, + 3269365386285466861, + 3271121084711600321, + 3272877725974217112, + 3274635310579634618, + 3276393839034442122, + 3278153311845500954, + 3279913729519944638, + 3281675092565179033, + 3283437401488882486, + 3285200656799005972, + 3286964859003773244, + 3288730008611680980, + 3290496106131498928, + 3292263152072270053, + 3294031146943310682, + 3295800091254210654, + 3297569985514833467, + 3299340830235316421, + 3301112625926070768, + 3302885373097781858, + 3304659072261409288, + 3306433723928187048, + 3308209328609623667, + 3309985886817502363, + 3311763399063881188, + 3313541865861093178, + 3315321287721746500, + 3317101665158724598, + 3318882998685186342, + 3320665288814566178, + 3322448536060574272, + 3324232740937196661, + 3326017903958695400, + 3327804025639608710, + 3329591106494751129, + 3331379147039213655, + 3333168147788363899, + 3334958109257846233, + 3336749031963581936, + 3338540916421769347, + 3340333763148884009, + 3342127572661678821, + 3343922345477184186, + 3345718082112708160, + 3347514783085836602, + 3349312448914433320, + 3351111080116640227, + 3352910677210877482, + 3354711240715843645, + 3356512771150515824, + 3358315269034149828, + 3360118734886280311, + 3361923169226720928, + 3363728572575564478, + 3365534945453183061, + 3367342288380228224, + 3369150601877631111, + 3370959886466602613, + 3372770142668633522, + 3374581371005494675, + 3376393571999237110, + 3378206746172192213, + 3380020894046971872, + 3381836016146468622, + 3383652112993855802, + 3385469185112587703, + 3387287233026399716, + 3389106257259308488, + 3390926258335612072, + 3392747236779890074, + 3394569193117003809, + 3396392127872096450, + 3398216041570593179, + 3400040934738201342, + 3401866807900910593, + 3403693661584993056, + 3405521496317003466, + 3407350312623779329, + 3409180111032441070, + 3411010892070392184, + 3412842656265319392, + 3414675404145192790, + 3416509136238266000, + 3418343853073076326, + 3420179555178444906, + 3422016243083476858, + 3423853917317561443, + 3425692578410372208, + 3427532226891867145, + 3429372863292288840, + 3431214488142164628, + 3433057101972306745, + 3434900705313812481, + 3436745298698064334, + 3438590882656730160, + 3440437457721763333, + 3442285024425402889, + 3444133583300173688, + 3445983134878886562, + 3447833679694638471, + 3449685218280812657, + 3451537751171078796, + 3453391278899393151, + 3455245801999998731, + 3457101321007425439, + 3458957836456490229, + 3460815348882297260, + 3462673858820238051, + 3464533366805991632, + 3466393873375524704, + 3468255379065091786, + 3470117884411235378, + 3471981389950786108, + 3473845896220862892, + 3475711403758873086, + 3477577913102512644, + 3479445424789766267, + 3481313939358907565, + 3483183457348499208, + 3485053979297393082, + 3486925505744730446, + 3488798037229942083, + 3490671574292748462, + 3492546117473159888, + 3494421667311476659, + 3496298224348289224, + 3498175789124478336, + 3500054362181215211, + 3501933944059961680, + 3503814535302470348, + 3505696136450784749, + 3507578748047239503, + 3509462370634460471, + 3511347004755364914, + 3513232650953161646, + 3515119309771351194, + 3517006981753725950, + 3518895667444370333, + 3520785367387660945, + 3522676082128266722, + 3524567812211149100, + 3526460558181562164, + 3528354320585052811, + 3530249099967460904, + 3532144896874919430, + 3534041711853854658, + 3535939545450986295, + 3537838398213327647, + 3539738270688185773, + 3541639163423161644, + 3543541076966150301, + 3545444011865341013, + 3547347968669217435, + 3549252947926557766, + 3551158950186434907, + 3553065975998216620, + 3554974025911565685, + 3556883100476440059, + 3558793200243093036, + 3560704325762073402, + 3562616477584225599, + 3564529656260689879, + 3566443862342902464, + 3568359096382595707, + 3570275358931798248, + 3572192650542835177, + 3574110971768328189, + 3576030323161195745, + 3577950705274653234, + 3579872118662213126, + 3581794563877685138, + 3583718041475176393, + 3585642552009091573, + 3587568096034133089, + 3589494674105301231, + 3591422286777894336, + 3593350934607508942, + 3595280618150039951, + 3597211337961680792, + 3599143094598923574, + 3601075888618559253, + 3603009720577677790, + 3604944591033668312, + 3606880500544219270, + 3608817449667318605, + 3610755438961253905, + 3612694468984612566, + 3614634540296281954, + 3616575653455449568, + 3618517809021603195, + 3620461007554531080, + 3622405249614322080, + 3624350535761365828, + 3626296866556352895, + 3628244242560274953, + 3630192664334424934, + 3632142132440397190, + 3634092647440087662, + 3636044209895694035, + 3637996820369715902, + 3639950479424954930, + 3641905187624515016, + 3643860945531802452, + 3645817753710526091, + 3647775612724697502, + 3649734523138631139, + 3651694485516944503, + 3653655500424558298, + 3655617568426696605, + 3657580690088887034, + 3659544865976960894, + 3661510096657053354, + 3663476382695603605, + 3665443724659355027, + 3667412123115355345, + 3669381578630956803, + 3671352091773816317, + 3673323663111895645, + 3675296293213461550, + 3677269982647085960, + 3679244731981646139, + 3681220541786324842, + 3683197412630610487, + 3685175345084297315, + 3687154339717485554, + 3689134397100581586, + 3691115517804298108, + 3693097702399654301, + 3695080951457975988, + 3697065265550895807, + 3699050645250353368, + 3701037091128595422, + 3703024603758176026, + 3705013183711956707, + 3707002831563106626, + 3708993547885102746, + 3710985333251729994, + 3712978188237081431, + 3714972113415558411, + 3716967109361870754, + 3718963176651036904, + 3720960315858384101, + 3722958527559548544, + 3724957812330475557, + 3726958170747419755, + 3728959603386945212, + 3730962110825925623, + 3732965693641544475, + 3734970352411295211, + 3736976087712981394, + 3738982900124716881, + 3740990790224925980, + 3742999758592343626, + 3745009805806015539, + 3747020932445298398, + 3749033139089860005, + 3751046426319679452, + 3753060794715047289, + 3755076244856565689, + 3757092777325148619, + 3759110392702022005, + 3761129091568723900, + 3763148874507104652, + 3765169742099327070, + 3767191694927866595, + 3769214733575511465, + 3771238858625362884, + 3773264070660835192, + 3775290370265656029, + 3777317758023866505, + 3779346234519821372, + 3781375800338189187, + 3783406456063952483, + 3785438202282407938, + 3787471039579166543, + 3789504968540153770, + 3791539989751609744, + 3793576103800089406, + 3795613311272462691, + 3797651612755914687, + 3799691008837945812, + 3801731500106371981, + 3803773087149324772, + 3805815770555251602, + 3807859550912915892, + 3809904428811397235, + 3811950404840091574, + 3813997479588711361, + 3816045653647285737, + 3818094927606160694, + 3820145302055999251, + 3822196777587781621, + 3824249354792805382, + 3826303034262685649, + 3828357816589355241, + 3830413702365064856, + 3832470692182383239, + 3834528786634197352, + 3836587986313712548, + 3838648291814452739, + 3840709703730260569, + 3842772222655297585, + 3844835849184044406, + 3846900583911300896, + 3848966427432186338, + 3851033380342139600, + 3853101443236919313, + 3855170616712604035, + 3857240901365592431, + 3859312297792603440, + 3861384806590676446, + 3863458428357171455, + 3865533163689769263, + 3867609013186471630, + 3869685977445601450, + 3871764057065802929, + 3873843252646041751, + 3875923564785605255, + 3878004994084102606, + 3880087541141464967, + 3882171206557945676, + 3884255990934120412, + 3886341894870887376, + 3888428918969467458, + 3890517063831404413, + 3892606330058565036, + 3894696718253139330, + 3896788229017640687, + 3898880862954906055, + 3900974620668096115, + 3903069502760695456, + 3905165509836512744, + 3907262642499680903, + 3909360901354657284, + 3911460287006223840, + 3913560800059487301, + 3915662441119879350, + 3917765210793156794, + 3919869109685401742, + 3921974138403021778, + 3924080297552750135, + 3926187587741645871, + 3928296009577094046, + 3930405563666805893, + 3932516250618818994, + 3934628071041497460, + 3936741025543532098, + 3938855114733940594, + 3940970339222067687, + 3943086699617585340, + 3945204196530492920, + 3947322830571117375, + 3949442602350113405, + 3951563512478463643, + 3953685561567478826, + 3955808750228797977, + 3957933079074388577, + 3960058548716546743, + 3962185159767897404, + 3964312912841394479, + 3966441808550321049, + 3968571847508289542, + 3970703030329241902, + 3972835357627449771, + 3974968830017514661, + 3977103448114368137, + 3979239212533271991, + 3981376123889818420, + 3983514182799930203, + 3985653389879860877, + 3987793745746194920, + 3989935251015847923, + 3992077906306066769, + 3994221712234429815, + 3996366669418847063, + 3998512778477560345, + 4000660040029143497, + 4002808454692502539, + 4004958023086875852, + 4007108745831834357, + 4009260623547281696, + 4011413656853454406, + 4013567846370922103, + 4015723192720587656, + 4017879696523687368, + 4020037358401791159, + 4022196178976802736, + 4024356158870959782, + 4026517298706834128, + 4028679599107331938, + 4030843060695693884, + 4033007684095495329, + 4035173469930646504, + 4037340418825392690, + 4039508531404314398, + 4041677808292327546, + 4043848250114683643, + 4046019857496969967, + 4048192631065109745, + 4050366571445362336, + 4052541679264323407, + 4054717955148925120, + 4056895399726436305, + 4059074013624462648, + 4061253797470946866, + 4063434751894168894, + 4065616877522746059, + 4067800174985633268, + 4069984644912123185, + 4072170287931846413, + 4074357104674771677, + 4076545095771206005, + 4078734261851794909, + 4080924603547522567, + 4083116121489712005, + 4085308816310025278, + 4087502688640463655, + 4089697739113367798, + 4091893968361417946, + 4094091377017634096, + 4096289965715376187, + 4098489735088344281, + 4100690685770578748, + 4102892818396460446, + 4105096133600710905, + 4107300632018392510, + 4109506314284908685, + 4111713181036004075, + 4113921232907764729, + 4116130470536618283, + 4118340894559334145, + 4120552505613023679, + 4122765304335140385, + 4124979291363480087, + 4127194467336181115, + 4129410832891724487, + 4131628388668934096, + 4133847135306976894, + 4136067073445363074, + 4138288203723946256, + 4140510526782923672, + 4142734043262836347, + 4144958753804569289, + 4147184659049351669, + 4149411759638757008, + 4151640056214703363, + 4153869549419453510, + 4156100239895615128, + 4158332128286140989, + 4160565215234329138, + 4162799501383823081, + 4165034987378611972, + 4167271673863030794, + 4169509561481760551, + 4171748650879828448, + 4173988942702608079, + 4176230437595819616, + 4178473136205529989, + 4180717039178153078, + 4182962147160449897, + 4185208460799528779, + 4187455980742845566, + 4189704707638203790, + 4191954642133754868, + 4194205784877998279, + 4196458136519781761, + 4198711697708301488, + 4200966469093102267, + 4203222451324077717, + 4205479645051470460, + 4207738050925872310, + 4209997669598224456, + 4212258501719817655, + 4214520547942292414, + 4216783808917639184, + 4219048285298198541, + 4221313977736661380, + 4223580886886069102, + 4225849013399813797, + 4228118357931638442, + 4230388921135637078, + 4232660703666255009, + 4234933706178288983, + 4237207929326887385, + 4239483373767550424, + 4241760040156130323, + 4244037929148831508, + 4246317041402210796, + 4248597377573177583, + 4250878938318994039, + 4253161724297275292, + 4255445736165989617, + 4257730974583458632, + 4260017440208357479, + 4262305133699715024, + 4264594055716914036, + 4266884206919691386, + 4269175587968138232, + 4271468199522700212, + 4273762042244177632, + 4276057116793725658, + 4278353423832854508, + 4280650964023429638, + 4282949738027671937, + 4285249746508157919, + 4287550990127819908, + 4289853469549946234, + 4292157185438181422, + 4294462138456526387, + 4296768329269338620, + 4299075758541332381, + 4301384426937578894, + 4303694335123506535, + 4306005483764901026, + 4308317873527905624, + 4310631505079021317, + 4312946379085107013, + 4315262496213379734, + 4317579857131414806, + 4319898462507146056, + 4322218313008865998, + 4324539409305226031, + 4326861752065236630, + 4329185341958267539, + 4331510179654047962, + 4333836265822666759, + 4336163601134572638, + 4338492186260574346, + 4340822021871840867, + 4343153108639901612, + 4345485447236646612, + 4347819038334326716, + 4350153882605553778, + 4352489980723300858, + 4354827333360902410, + 4357165941192054482, + 4359505804890814904, + 4361846925131603487, + 4364189302589202213, + 4366532937938755436, + 4368877831855770069, + 4371223985016115786, + 4373571398096025208, + 4375920071772094110, + 4378270006721281602, + 4380621203620910337, + 4382973663148666696, + 4385327385982600992, + 4387682372801127658, + 4390038624283025446, + 4392396141107437625, + 4394754923953872172, + 4397114973502201969, + 4399476290432665003, + 4401838875425864556, + 4404202729162769407, + 4406567852324714024, + 4408934245593398761, + 4411301909650890057, + 4413670845179620630, + 4416041052862389675, + 4418412533382363060, + 4420785287423073525, + 4423159315668420875, + 4425534618802672181, + 4427911197510461975, + 4430289052476792449, + 4432668184387033650, + 4435048593926923680, + 4437430281782568893, + 4439813248640444092, + 4442197495187392727, + 4444583022110627094, + 4446969830097728531, + 4449357919836647619, + 4451747292015704378, + 4454137947323588468, + 4456529886449359382, + 4458923110082446652, + 4461317618912650041, + 4463713413630139749, + 4466110494925456602, + 4468508863489512263, + 4470908520013589420, + 4473309465189341993, + 4475711699708795329, + 4478115224264346405, + 4480520039548764023, + 4482926146255189015, + 4485333545077134438, + 4487742236708485776, + 4490152221843501142, + 4492563501176811474, + 4494976075403420737, + 4497389945218706127, + 4499805111318418263, + 4502221574398681396, + 4504639335155993606, + 4507058394287227002, + 4509478752489627923, + 4511900410460817142, + 4514323368898790063, + 4516747628501916926, + 4519173189968943003, + 4521600053998988807, + 4524028221291550286, + 4526457692546499028, + 4528888468464082465, + 4531320549744924071, + 4533753937090023564, + 4536188631200757111, + 4538624632778877529, + 4541061942526514485, + 4543500561146174701, + 4545940489340742158, + 4548381727813478292, + 4550824277268022204, + 4553268138408390859, + 4555713311938979290, + 4558159798564560800, + 4560607598990287166, + 4563056713921688843, + 4565507144064675165, + 4567958890125534552, + 4570411952810934709, + 4572866332827922835, + 4575322030883925821, + 4577779047686750460, + 4580237383944583644, + 4582697040365992577, + 4585158017659924970, + 4587620316535709252, + 4590083937703054770, + 4592548881872051998, + 4595015149753172737, + 4597482742057270324, + 4599951659495579834, + 4602421902779718286, + 4604893472621684847, + 4607366369733861041, + 4609840594829010949, + 4612316148620281419, + 4614793031821202267, + 4617271245145686488, + 4619750789308030457, + 4622231665022914139, + 4624713873005401290, + 4627197413970939669, + 4629682288635361240, + 4632168497714882378, + 4634656041926104079, + 4637144921986012165, + 4639635138611977488, + 4642126692521756141, + 4644619584433489660, + 4647113815065705238, + 4649609385137315923, + 4652106295367620835, + 4654604546476305366, + 4657104139183441389, + 4659605074209487468, + 4662107352275289063, + 4664610974102078740, + 4667115940411476378, + 4669622251925489375, + 4672129909366512859, + 4674638913457329894, + 4677149264921111692, + 4679660964481417816, + 4682174012862196392, + 4684688410787784319, + 4687204158982907472, + 4689721258172680919, + 4692239709082609124, + 4694759512438586155, + 4697280668966895901, + 4699803179394212273, + 4702327044447599417, + 4704852264854511926, + 4707378841342795044, + 4709906774640684880, + 4712436065476808617, + 4714966714580184722, + 4717498722680223156, + 4720032090506725582, + 4722566818789885580, + 4725102908260288854, + 4727640359648913444, + 4730179173687129935, + 4732719351106701671, + 4735260892639784961, + 4737803799018929296, + 4740348070977077555, + 4742893709247566220, + 4745440714564125584, + 4747989087660879965, + 4750538829272347918, + 4753089940133442443, + 4755642420979471202, + 4758196272546136727, + 4760751495569536633, + 4763308090786163830, + 4765866058932906738, + 4768425400747049495, + 4770986116966272173, + 4773548208328650988, + 4776111675572658517, + 4778676519437163903, + 4781242740661433077, + 4783810339985128965, + 4786379318148311705, + 4788949675891438855, + 4791521413955365613, + 4794094533081345027, + 4796669034011028208, + 4799244917486464544, + 4801822184250101917, + 4804400835044786914, + 4806980870613765040, + 4809562291700680936, + 4812145099049578592, + 4814729293404901557, + 4817314875511493163, + 4819901846114596728, + 4822490205959855782, + 4825079955793314274, + 4827671096361416791, + 4830263628411008772, + 4832857552689336721, + 4835452869944048428, + 4838049580923193178, + 4840647686375221973, + 4843247187048987742, + 4845848083693745560, + 4848450377059152863, + 4851054067895269665, + 4853659156952558773, + 4856265644981886003, + 4858873532734520400, + 4861482820962134448, + 4864093510416804294, + 4866705601851009959, + 4869319096017635557, + 4871933993669969514, + 4874550295561704782, + 4877168002446939056, + 4879787115080174996, + 4882407634216320439, + 4885029560610688619, + 4887652895018998385, + 4890277638197374420, + 4892903790902347454, + 4895531353890854488, + 4898160327920239008, + 4900790713748251206, + 4903422512133048198, + 4906055723833194239, + 4908690349607660946, + 4911326390215827517, + 4913963846417480944, + 4916602718972816240, + 4919243008642436653, + 4921884716187353884, + 4924527842368988311, + 4927172387949169207, + 4929818353690134957, + 4932465740354533280, + 4935114548705421449, + 4937764779506266509, + 4940416433520945499, + 4943069511513745672, + 4945724014249364714, + 4948379942492910965, + 4951037297009903640, + 4953696078566273050, + 4956356287928360821, + 4959017925862920117, + 4961680993137115859, + 4964345490518524947, + 4967011418775136484, + 4969678778675351990, + 4972347570987985633, + 4975017796482264443, + 4977689455927828536, + 4980362550094731339, + 4983037079753439807, + 4985713045674834648, + 4988390448630210545, + 4991069289391276377, + 4993749568730155443, + 4996431287419385683, + 4999114446231919903, + 5001799045941125996, + 5004485087320787163, + 5007172571145102140, + 5009861498188685420, + 5012551869226567475, + 5015243685034194979, + 5017936946387431033, + 5020631654062555390, + 5023327808836264675, + 5026025411485672611, + 5028724462788310243, + 5031424963522126162, + 5034126914465486729, + 5036830316397176299, + 5039535170096397447, + 5042241476342771190, + 5044949235916337216, + 5047658449597554102, + 5050369118167299547, + 5053081242406870590, + 5055794823097983840, + 5058509861022775699, + 5061226356963802588, + 5063944311704041172, + 5066663726026888589, + 5069384600716162669, + 5072106936556102167, + 5074830734331366985, + 5077555994827038399, + 5080282718828619286, + 5083010907122034348, + 5085740560493630344, + 5088471679730176310, + 5091204265618863789, + 5093938318947307060, + 5096673840503543360, + 5099410831076033115, + 5102149291453660168, + 5104889222425732001, + 5107630624781979967, + 5110373499312559519, + 5113117846808050434, + 5115863668059457040, + 5118610963858208449, + 5121359734996158782, + 5124109982265587396, + 5126861706459199116, + 5129614908370124461, + 5132369588791919871, + 5135125748518567939, + 5137883388344477641, + 5140642509064484558, + 5143403111473851114, + 5146165196368266797, + 5148928764543848396, + 5151693816797140224, + 5154460353925114352, + 5157228376725170835, + 5159997885995137945, + 5162768882533272401, + 5165541367138259597, + 5168315340609213831, + 5171090803745678540, + 5173867757347626527, + 5176646202215460192, + 5179426139150011765, + 5182207568952543533, + 5184990492424748072, + 5187774910368748482, + 5190560823587098614, + 5193348232882783301, + 5196137139059218593, + 5198927542920251984, + 5201719445270162650, + 5204512846913661673, + 5207307748655892280, + 5210104151302430070, + 5212902055659283249, + 5215701462532892863, + 5218502372730133026, + 5221304787058311157, + 5224108706325168213, + 5226914131338878916, + 5229721062908051995, + 5232529501841730410, + 5235339448949391591, + 5238150905040947669, + 5240963870926745710, + 5243778347417567949, + 5246594335324632024, + 5249411835459591208, + 5252230848634534643, + 5255051375661987578, + 5257873417354911597, + 5260696974526704859, + 5263522047991202329, + 5266348638562676014, + 5269176747055835197, + 5272006374285826673, + 5274837521068234982, + 5277670188219082646, + 5280504376554830402, + 5283340086892377441, + 5286177320049061639, + 5289016076842659796, + 5291856358091387870, + 5294698164613901213, + 5297541497229294809, + 5300386356757103505, + 5303232744017302254, + 5306080659830306345, + 5308930105016971645, + 5311781080398594831, + 5314633586796913630, + 5317487625034107054, + 5320343195932795636, + 5323200300316041671, + 5326058939007349450, + 5328919112830665499, + 5331780822610378812, + 5334644069171321098, + 5337508853338767008, + 5340375175938434381, + 5343243037796484478, + 5346112439739522221, + 5348983382594596430, + 5351855867189200066, + 5354729894351270462, + 5357605464909189569, + 5360482579691784190, + 5363361239528326222, + 5366241445248532891, + 5369123197682566996, + 5372006497661037145, + 5374891346014997995, + 5377777743575950492, + 5380665691175842110, + 5383555189647067094, + 5386446239822466694, + 5389338842535329409, + 5392232998619391229, + 5395128708908835868, + 5398025974238295015, + 5400924795442848563, + 5403825173358024860, + 5406727108819800944, + 5409630602664602783, + 5412535655729305521, + 5415442268851233716, + 5418350442868161581, + 5421260178618313227, + 5424171476940362905, + 5427084338673435244, + 5429998764657105499, + 5432914755731399786, + 5435832312736795330, + 5438751436514220704, + 5441672127905056074, + 5444594387751133437, + 5447518216894736868, + 5450443616178602761, + 5453370586445920073, + 5456299128540330565, + 5459229243305929047, + 5462160931587263620, + 5465094194229335921, + 5468029032077601363, + 5470965445977969386, + 5473903436776803692, + 5476843005320922494, + 5479784152457598760, + 5482726879034560456, + 5485671185899990791, + 5488617073902528459, + 5491564543891267890, + 5494513596715759486, + 5497464233226009873, + 5500416454272482143, + 5503370260706096098, + 5506325653378228499, + 5509282633140713308, + 5512241200845841934, + 5515201357346363480, + 5518163103495484989, + 5521126440146871688, + 5524091368154647234, + 5527057888373393963, + 5530026001658153135, + 5532995708864425178, + 5535967010848169939, + 5538939908465806925, + 5541914402574215557, + 5544890494030735410, + 5547868183693166465, + 5550847472419769355, + 5553828361069265610, + 5556810850500837907, + 5559794941574130318, + 5562780635149248557, + 5565767932086760225, + 5568756833247695064, + 5571747339493545200, + 5574739451686265395, + 5577733170688273291, + 5580728497362449663, + 5583725432572138666, + 5586723977181148084, + 5589724132053749578, + 5592725898054678936, + 5595729276049136323, + 5598734266902786528, + 5601740871481759216, + 5604749090652649177, + 5607758925282516575, + 5610770376238887198, + 5613783444389752709, + 5616798130603570895, + 5619814435749265919, + 5622832360696228568, + 5625851906314316506, + 5628873073473854522, + 5631895863045634786, + 5634920275900917092, + 5637946312911429117, + 5640973974949366667, + 5644003262887393932, + 5647034177598643734, + 5650066719956717782, + 5653100890835686920, + 5656136691110091385, + 5659174121654941052, + 5662213183345715690, + 5665253877058365215, + 5668296203669309941, + 5671340164055440831, + 5674385759094119754, + 5677432989663179735, + 5680481856640925208, + 5683532360906132270, + 5686584503338048934, + 5689638284816395382, + 5692693706221364219, + 5695750768433620728, + 5698809472334303122, + 5701869818805022796, + 5704931808727864587, + 5707995442985387023, + 5711060722460622579, + 5714127648037077934, + 5717196220598734221, + 5720266441030047284, + 5723338310215947936, + 5726411829041842207, + 5729486998393611607, + 5732563819157613376, + 5735642292220680742, + 5738722418470123174, + 5741804198793726642, + 5744887634079753868, + 5747972725216944586, + 5751059473094515797, + 5754147878602162024, + 5757237942630055570, + 5760329666068846772, + 5763423049809664262, + 5766518094744115221, + 5769614801764285636, + 5772713171762740557, + 5775813205632524356, + 5778914904267160983, + 5782018268560654223, + 5785123299407487956, + 5788229997702626412, + 5791338364341514431, + 5794448400220077719, + 5797560106234723110, + 5800673483282338820, + 5803788532260294708, + 5806905254066442534, + 5810023649599116219, + 5813143719757132101, + 5816265465439789198, + 5819388887546869463, + 5822513986978638047, + 5825640764635843556, + 5828769221419718311, + 5831899358231978611, + 5835031175974824988, + 5838164675550942469, + 5841299857863500838, + 5844436723816154894, + 5847575274313044711, + 5850715510258795902, + 5853857432558519877, + 5857001042117814103, + 5860146339842762368, + 5863293326639935040, + 5866442003416389330, + 5869592371079669550, + 5872744430537807379, + 5875898182699322122, + 5879053628473220973, + 5882210768768999276, + 5885369604496640788, + 5888530136566617940, + 5891692365889892103, + 5894856293377913844, + 5898021919942623197, + 5901189246496449918, + 5904358273952313754, + 5907529003223624701, + 5910701435224283273, + 5913875570868680761, + 5917051411071699496, + 5920228956748713118, + 5923408208815586834, + 5926589168188677685, + 5929771835784834810, + 5932956212521399709, +]; diff --git a/src/heston.rs b/src/heston.rs index b2bb5fb..8d6a283 100644 --- a/src/heston.rs +++ b/src/heston.rs @@ -1,36 +1,102 @@ +#[cfg(all(test, feature = "complex"))] +use crate::arithmetic::{fp_div_i, fp_mul, fp_sqrt}; +use crate::arithmetic::{fp_mul_i, isqrt_u128}; use crate::constants::*; use crate::error::SolMathError; -use crate::arithmetic::{fp_mul, fp_mul_i, fp_div_i, fp_sqrt}; -use crate::transcendental::{exp_fixed_i, expm1_fixed}; -use crate::hp::bs_full_hp; +use crate::hp::{bs_full_hp, exp_fixed_hp, fp_div_hp_safe}; +#[cfg(all(test, feature = "complex"))] use crate::i64_cf::{heston_cv_node_h, to_h, to_h_i, SCALE_TO_H}; - -// Used only by #[cfg(test)] heston_price_cf_raw -#[cfg(test)] -use crate::transcendental::ln_fixed_i; -#[cfg(test)] -use crate::trig::{sincos_fixed, cos_fixed}; -#[cfg(test)] -use crate::complex::{Complex, complex_sqrt}; +use crate::overflow::checked_mul_div_rem_u; +use crate::transcendental::exp_fixed_i; +#[cfg(all(test, feature = "complex"))] +use crate::{complex_sqrt, cos_fixed, ln_fixed_i, sincos_fixed, Complex}; // ============================================================ -// Heston stochastic volatility pricing. +// Heston deterministic-limit execution. // -// Three-path architecture: -// 1. Degenerate: t=0, s=0, k=0. -// 2. BS path (ξ²T < 0.01): BS(σ̄) via bs_full_hp. ~130K CU. -// 3. CV path: BS(σ_eff) + 21-node DE quadrature of (φ_BS − φ_H) -// with i64 Heston CF + i128 BS CF. Target: ~300K CU. +// Executable architecture: +// 1. Expiry: intrinsic value. +// 2. Deterministic variance (xi == 0): exact reduction to BS with integrated +// CIR variance, evaluated with a cancellation-safe HP formula. +// 3. Stochastic variance (xi > 0): fail closed. No unqualified stochastic +// approximation is shipped in the release implementation. // ============================================================ -const HESTON_BS_THRESHOLD: u128 = 10_000_000_000; // 0.01 at SCALE +const HESTON_MAX_RATE: u128 = 5 * SCALE; +const HESTON_MAX_TIME: u128 = 100 * SCALE; +const HESTON_MAX_VARIANCE: u128 = 4 * SCALE; +const HESTON_MAX_KAPPA: u128 = 20 * SCALE; +const HESTON_MAX_XI: u128 = 5 * SCALE; + +fn discounted_strike(k: u128, r: u128, t: u128) -> Result { + let rt = fp_mul_i(r as i128, t as i128)?; + let value = fp_mul_i(k as i128, exp_fixed_i(-rt)?)?; + if value < 0 { + Err(SolMathError::Overflow) + } else { + Ok(value as u128) + } +} -/// Heston stochastic-volatility European option price. +/// Enforce European no-arbitrage bounds and derive put from call parity. +/// CV values outside the bounds by more than rounding noise fail closed. +fn parity_from_call( + raw_call: i128, + s: u128, + k_disc: u128, + max_bound_error: Option, +) -> Result<(u128, u128), SolMathError> { + let lower = s.saturating_sub(k_disc); + let upper = s; + let call = if raw_call < lower as i128 { + let miss = (lower as i128 - raw_call).unsigned_abs(); + if max_bound_error.is_some_and(|limit| miss > limit) { + return Err(SolMathError::NoConvergence); + } + lower + } else if raw_call > upper as i128 { + let miss = (raw_call - upper as i128).unsigned_abs(); + if max_bound_error.is_some_and(|limit| miss > limit) { + return Err(SolMathError::NoConvergence); + } + upper + } else { + raw_call as u128 + }; + let put = if call >= s { + call.checked_sub(s).and_then(|v| v.checked_add(k_disc)) + } else { + k_disc.checked_sub(s - call) + } + .ok_or(SolMathError::Overflow)?; + Ok((call, put)) +} + +fn heston_bs_approx( + s: u128, + k: u128, + r: u128, + t: u128, + v0: u128, + kappa: u128, + theta: u128, +) -> Result<(u128, u128), SolMathError> { + let sigma_bar = cir_rms_vol(v0, kappa, theta, t)?; + let k_disc = discounted_strike(k, r, t)?; + if sigma_bar == 0 { + return parity_from_call(s.saturating_sub(k_disc) as i128, s, k_disc, None); + } + let bs = bs_full_hp(s, k, r, sigma_bar, t)?; + parity_from_call(bs.call as i128, s, k_disc, None) +} + +/// Fail-closed Heston European option price. /// -/// Three-path architecture: -/// 1. **Degenerate**: t=0, s=0, or k=0 -> intrinsic value. -/// 2. **BS path**: when xi^2*T < 0.01, uses BS(sigma_bar) via `bs_full_hp`. ~130K CU. -/// 3. **CV path**: BS control variate + 21-node DE quadrature. ~410-430K CU. +/// Positive-expiry execution is supported only for deterministic variance +/// (`xi == 0`). In that case the CIR variance path is deterministic and its +/// integrated variance reduces exactly to a Black-Scholes total variance. +/// Every positive-expiry stochastic case (`xi > 0`) returns `NoConvergence`. +/// At expiry the function returns intrinsic value. /// /// # Parameters /// All at SCALE (`u128`) except `rho` (`i128`): @@ -48,66 +114,80 @@ const HESTON_BS_THRESHOLD: u128 = 10_000_000_000; // 0.01 at SCALE /// `(call, put)` prices at SCALE. /// /// # Errors -/// - `Overflow` if intermediate arithmetic overflows. +/// - `DomainError` if `rho` is outside the open interval `(-SCALE_I, SCALE_I)`. +/// The checked i64-CF domain also caps r≤5, T≤100, v0/theta≤4, +/// kappa≤20, and xi≤5. +/// - `Overflow` if an input cannot be represented by the signed fixed-point +/// implementation or deterministic intermediate arithmetic overflows. +/// - `NoConvergence` for every positive-expiry stochastic case (`xi > 0`). +/// Call and put in accepted deterministic cases are returned from one leg via +/// put-call parity. /// /// # Accuracy -/// $0.002-$0.007 typical, $0.018 worst case vs QuantLib AnalyticHestonEngine. +/// The accepted `xi == 0` path uses the exact deterministic-variance reduction; +/// numerical error comes from the fixed-point integrated variance and HP +/// Black-Scholes primitives. Stochastic Heston approximation results are not +/// exposed by this API. /// /// # CU Cost -/// 410-430K CU (CV path). 130K CU (BS fallback when xi^2*T < 0.01). +/// Final SBF audit: accepted deterministic cases averaged 119,725 CU and +/// maxed at 190,698; stochastic fail-closed cases used at most 283 CU. pub fn heston_price( - s: u128, k: u128, r: u128, t: u128, - v0: u128, kappa: u128, theta: u128, xi: u128, + s: u128, + k: u128, + r: u128, + t: u128, + v0: u128, + kappa: u128, + theta: u128, + xi: u128, rho: i128, ) -> Result<(u128, u128), SolMathError> { - const MAX_H_INPUT: u128 = ((i64::MAX as i128) * SCALE_TO_H) as u128; - - if s > i128::MAX as u128 || k > i128::MAX as u128 || r > i128::MAX as u128 - || t > i128::MAX as u128 || v0 > i128::MAX as u128 || kappa > i128::MAX as u128 - || theta > i128::MAX as u128 || xi > i128::MAX as u128 + if s > i128::MAX as u128 + || k > i128::MAX as u128 + || r > i128::MAX as u128 + || t > i128::MAX as u128 + || v0 > i128::MAX as u128 + || kappa > i128::MAX as u128 + || theta > i128::MAX as u128 + || xi > i128::MAX as u128 { return Err(SolMathError::Overflow); } if rho <= -SCALE_I || rho >= SCALE_I { return Err(SolMathError::DomainError); } - if s > MAX_H_INPUT || k > MAX_H_INPUT || r > MAX_H_INPUT || t > MAX_H_INPUT - || v0 > MAX_H_INPUT || kappa > MAX_H_INPUT || theta > MAX_H_INPUT || xi > MAX_H_INPUT + if r > HESTON_MAX_RATE + || t > HESTON_MAX_TIME + || v0 > HESTON_MAX_VARIANCE + || theta > HESTON_MAX_VARIANCE + || kappa > HESTON_MAX_KAPPA + || xi > HESTON_MAX_XI { - return Err(SolMathError::Overflow); + return Err(SolMathError::DomainError); } if t == 0 { let call = if s > k { s - k } else { 0 }; let put = if k > s { k - s } else { 0 }; return Ok((call, put)); } + if xi != 0 { + return Err(SolMathError::NoConvergence); + } if s == 0 { let r_t = fp_mul_i(r as i128, t as i128)?; let put = fp_mul_i(k as i128, exp_fixed_i(-r_t)?)?; return Ok((0, if put > 0 { put as u128 } else { 0 })); } - if k == 0 { return Ok((s, 0)); } - - let xi_sq_t = fp_mul(fp_mul(xi, xi)?, t)?; - if xi == 0 || xi_sq_t < HESTON_BS_THRESHOLD { - let sigma_bar = cir_rms_vol(v0, kappa, theta, t)?; - if sigma_bar == 0 { - let r_t = fp_mul_i(r as i128, t as i128)?; - let k_disc = fp_mul_i(k as i128, exp_fixed_i(-r_t)?)? as u128; - let call = if s > k_disc { s - k_disc } else { 0 }; - let put = if k_disc > s { k_disc - s } else { 0 }; - return Ok((call, put)); - } - let bs = bs_full_hp(s, k, r, sigma_bar, t)?; - return Ok((bs.call, bs.put)); + if k == 0 { + return Ok((s, 0)); } - - heston_price_cv(s, k, r, t, v0, kappa, theta, xi, rho) + heston_bs_approx(s, k, r, t, v0, kappa, theta) } /// Fast signed multiply — no overflow check. /// CF intermediates bounded: max product < 9e29 ≪ i128::MAX (1.7e38). -#[cfg(test)] +#[cfg(all(test, feature = "complex"))] #[inline(always)] fn fmul(a: i128, b: i128) -> i128 { a * b / SCALE_I @@ -122,41 +202,126 @@ fn fmul(a: i128, b: i128) -> i128 { // The entire 21-node loop — CF eval, weight multiply, accumulation — // runs at SCALE_6 in i64. // -// CU budget: +// Historical research-only stochastic-CF estimate (~410-430K); this path is +// unreachable from public positive-expiry execution and is not a CU contract: // 130K bs_full_hp // 30K i128 setup (2× ln, 1× exp, 1× sqrt, precomputes) // 5K downscale 9 params // 63K 21 × 3K fully-i64 loop // 2K upscale + prefactor -// ≈230K total // ============================================================ /// 21-node DE nodes at SCALE_H (2^20). Precomputed: DE_NODES[i] / SCALE_TO_H. +#[cfg(all(test, feature = "complex"))] const DE_NODES_H: [i64; 21] = [ - 0, 5, 78, 661, 3_518, 13_091, - 36_985, 84_677, 165_529, 288_138, 462_497, 705_139, - 1_048_576, 1_559_291, 2_377_199, 3_815_640, 6_641_838, 12_984_058, - 29_728_064, 83_979_424, 312_444_416, + 0, + 5, + 78, + 661, + 3_518, + 13_091, + 36_985, + 84_677, + 165_529, + 288_138, + 462_497, + 705_139, + 1_048_576, + 1_559_291, + 2_377_199, + 3_815_640, + 6_641_838, + 12_984_058, + 29_728_064, + 83_979_424, + 312_444_416, ]; /// 21-node DE weights at SCALE_H (2^20). Precomputed: DE_WEIGHTS[i] / SCALE_TO_H. +#[cfg(all(test, feature = "complex"))] const DE_WEIGHTS_H: [i64; 21] = [ - 0, 15, 188, 1_245, 5_197, 15_237, - 34_162, 62_792, 100_303, 146_490, 204_809, 285_571, - 411_774, 631_479, 1_052_649, 1_939_833, 4_024_659, 9_627_695, - 27_459_829, 97_745_668, 461_585_778, + 0, + 15, + 188, + 1_245, + 5_197, + 15_237, + 34_162, + 62_792, + 100_303, + 146_490, + 204_809, + 285_571, + 411_774, + 631_479, + 1_052_649, + 1_939_833, + 4_024_659, + 9_627_695, + 27_459_829, + 97_745_668, + 461_585_778, ]; /// Heston via Andersen-Piterbarg control variate. /// BS(σ_eff) in i128. Entire 21-node DE loop at SCALE_H = 2^20 (shift-only mul). +#[allow(dead_code)] // Retained for research/reference work; public stochastic pricing is disabled. +#[cfg(all(test, feature = "complex"))] fn heston_price_cv( - s: u128, k: u128, r: u128, t: u128, - v0: u128, kappa: u128, theta: u128, xi: u128, + s: u128, + k: u128, + r: u128, + t: u128, + v0: u128, + kappa: u128, + theta: u128, + xi: u128, rho: i128, ) -> Result<(u128, u128), SolMathError> { - // ── i128: σ_eff + BS price (~140K CU) ── + // ── Guard the i64 SCALE_H fast path ── + // The 21-node loop runs at SCALE_H = 2^20 in i64: any product of two + // quantities (in real terms) must stay <= i64::MAX >> 20 ≈ 8.8e12, or + // mul_h wraps silently via `as i64` and the post-hoc correction bound can + // let a plausible-looking wrong price escape. Reject inputs whose loop + // products exceed the representable (and reference-tested) range. + const MAX_H_PRODUCT: u128 = ((i64::MAX >> 20) as u128) * SCALE; let sigma_eff_sq = cir_expected_var(v0, kappa, theta, t)?; + if sigma_eff_sq == 0 { + let k_disc = discounted_strike(k, r, t)?; + return parity_from_call(s.saturating_sub(k_disc) as i128, s, k_disc, None); + } + let seff_sq_t = fp_mul(sigma_eff_sq, t)?; + let xi_sq = fp_mul(xi, xi)?; + let kappa_theta = fp_mul(kappa, theta)?; + if fp_mul(s, k)? > MAX_H_PRODUCT + || fp_mul(r, t)? > MAX_H_PRODUCT + || v0 > MAX_H_PRODUCT + || fp_mul(kappa, kappa)? > MAX_H_PRODUCT + || kappa_theta > MAX_H_PRODUCT + || fp_mul(kappa, t)? > MAX_H_PRODUCT + || xi_sq > MAX_H_PRODUCT + || fp_mul(xi, t)? > MAX_H_PRODUCT + || seff_sq_t > MAX_H_PRODUCT + { + return Err(SolMathError::Overflow); + } + // The CF's loop-invariant mu = κθ/ξ² (and its t-product) must also be + // representable — tiny ξ with large κθ would otherwise wrap in div_h. + if xi_sq > 0 { + let mu = crate::arithmetic::fp_div(kappa_theta, xi_sq)?; + if mu > MAX_H_PRODUCT || fp_mul(mu, t)? > MAX_H_PRODUCT { + return Err(SolMathError::Overflow); + } + } + // ln_h(0) returns an i64::MIN sentinel that would poison the unchecked + // i64 arithmetic below; values that downscale to zero are out of domain. + if to_h(s) == 0 || to_h(k) == 0 { + return Err(SolMathError::DomainError); + } + + // ── i128: σ_eff + BS price (~140K CU) ── + let sigma_eff = fp_sqrt(sigma_eff_sq)?; let bs = bs_full_hp(s, k, r, sigma_eff, t)?; @@ -171,9 +336,9 @@ fn heston_price_cv( let thetah = to_h(theta); let xih = to_h(xi); let rhoh = to_h_i(rho); - let seff_sq_t_half_h = to_h(fp_mul(sigma_eff_sq, t)? / 2); + let seff_sq_t_half_h = to_h(seff_sq_t / 2); - use crate::i64_cf::{exp_h_pub, ln_h_pub, sqrt_h_pub, mul_h_pub, div_h_pub}; + use crate::i64_cf::{div_h_pub, exp_h_pub, ln_h_pub, mul_h_pub, sqrt_h_pub}; let r_t_h = mul_h_pub(rh, th); let x = ln_h_pub(sh) - ln_h_pub(kh) + r_t_h; let disc_h = exp_h_pub(-r_t_h); @@ -186,31 +351,44 @@ fn heston_price_cv( let xi_sq_h = mul_h_pub(xih, xih); let rho_sq_h = mul_h_pub(rhoh, rhoh); let xi_sq_1mrho_h = mul_h_pub(xi_sq_h, crate::i64_cf::SH_I64 - rho_sq_h); - let mu_h = if xi_sq_h != 0 { div_h_pub(mul_h_pub(kappah, thetah), xi_sq_h) } else { 0 }; + let mu_h = if xi_sq_h != 0 { + div_h_pub(mul_h_pub(kappah, thetah), xi_sq_h) + } else { + 0 + }; // DE loop — skip first 3 + last 2 (negligible contribution) const FIRST: usize = 3; const LAST: usize = 19; - let mut sum: i64 = 0; + let mut sum: i128 = 0; let mut idx = FIRST; while idx < LAST { + // Nodes/weights at indices 3..19 are all strictly positive constants. let u = DE_NODES_H[idx]; let w = DE_WEIGHTS_H[idx]; - if u > 0 && w > 0 { - let f = heston_cv_node_h( - u, x, th, v0h, - m_re_h, m_im_coeff_h, xi_sq_h, xi_sq_1mrho_h, - mu_h, seff_sq_t_half_h, - ); - // w ≤ DE_WEIGHTS_H max ≈ 4.6e8 (i64), f is the CF node result at SCALE_H (2^20); - // product fits i128 easily (< 4.6e8 * 2^63 ≪ i128::MAX), then shifted back by 20 bits. - sum += ((w as i128 * f as i128) >> 20) as i64; - } + let f = heston_cv_node_h( + u, + x, + th, + v0h, + m_re_h, + m_im_coeff_h, + xi_sq_h, + xi_sq_1mrho_h, + mu_h, + seff_sq_t_half_h, + ); + // w ≤ DE_WEIGHTS_H max ≈ 4.6e8 (i64), f is the CF node result at SCALE_H (2^20); + // product fits i128 easily (< 4.6e8 * 2^63 ≪ i128::MAX), then shifted back by 20 bits. + sum = sum + .checked_add((w as i128 * f as i128) >> 20) + .ok_or(SolMathError::Overflow)?; idx += 1; } // prefactor: √(SK·disc)/π × correction - let prefactor_h = div_h_pub(mul_h_pub(sqrt_sk_disc_h, sum), crate::i64_cf::PI_H_PUB); + let sum_h = i64::try_from(sum).map_err(|_| SolMathError::Overflow)?; + let prefactor_h = div_h_pub(mul_h_pub(sqrt_sk_disc_h, sum_h), crate::i64_cf::PI_H_PUB); // ── Upscale single result to i128, add to BS ── // prefactor_h is i64 at SCALE_H (2^20); SCALE_TO_H = SCALE / 2^20 = 1e12/2^20 ≈ 9.5e5; @@ -218,27 +396,37 @@ fn heston_price_cv( let correction_128 = prefactor_h as i128 * SCALE_TO_H; // Post-hoc safety: the CV correction is a difference from BS — it should be - // small relative to the spot price. If i64 CF overflow produced garbage, + // small relative to the option scale. If i64 CF overflow produced garbage, // the correction will be unreasonably large. Reject rather than misprice. - // Bound: correction should not exceed spot price (conservative). - if correction_128.unsigned_abs() > s as u128 { + // Bound against max(s, k): a put's value approaches k, so bounding by s + // alone would fail closed on legitimate deep-OTM inputs (tiny s, large k). + if correction_128.unsigned_abs() > s.max(k) { return Err(SolMathError::Overflow); } - let call = (bs.call as i128 + correction_128).max(0) as u128; - let put = (bs.put as i128 + correction_128).max(0) as u128; - - Ok((call, put)) + let raw_call = (bs.call as i128) + .checked_add(correction_128) + .ok_or(SolMathError::Overflow)?; + // The DE/CV approximation must not escape hard arbitrage bounds. Allow + // only a tiny rounding tolerance; larger violations signal non-convergence. + let tolerance = s.max(k) / 1_000; // 0.1% of the option notional + parity_from_call(raw_call, s, discounted_strike(k, r, t)?, Some(tolerance)) } // ============================================================ // Raw Lewis CF path (21-node DE, i128) — test reference only // ============================================================ -#[cfg(test)] +#[cfg(all(test, feature = "complex"))] fn heston_price_cf_raw( - s: u128, k: u128, r: u128, t: u128, - v0: u128, kappa: u128, theta: u128, xi: u128, + s: u128, + k: u128, + r: u128, + t: u128, + v0: u128, + kappa: u128, + theta: u128, + xi: u128, rho: i128, ) -> Result<(u128, u128), SolMathError> { let s_i = s as i128; @@ -299,27 +487,36 @@ fn heston_price_cf_raw( let dn_im = fmul(uq, exp_dt_im); let p_mod2 = fmul(p_re, p_re) + fmul(p_im, p_im); let (d_coeff_re, d_coeff_im) = if p_mod2 != 0 { - (fp_div_i(fmul(dn_re, p_re) + fmul(dn_im, p_im), p_mod2).unwrap(), - fp_div_i(fmul(dn_im, p_re) - fmul(dn_re, p_im), p_mod2).unwrap()) - } else { (0, 0) }; + ( + fp_div_i(fmul(dn_re, p_re) + fmul(dn_im, p_im), p_mod2).unwrap(), + fp_div_i(fmul(dn_im, p_re) - fmul(dn_re, p_im), p_mod2).unwrap(), + ) + } else { + (0, 0) + }; let ratio_re = if p_mod2 != 0 { - fp_div_i(fmul(2*d.re, p_re) + fmul(2*d.im, p_im), p_mod2).unwrap() - } else { SCALE_I }; + fp_div_i(fmul(2 * d.re, p_re) + fmul(2 * d.im, p_im), p_mod2).unwrap() + } else { + SCALE_I + }; let ratio_im = if p_mod2 != 0 { - fp_div_i(fmul(2*d.im, p_re) - fmul(2*d.re, p_im), p_mod2).unwrap() - } else { 0 }; - let ratio_mod = fp_sqrt( - (fmul(ratio_re, ratio_re) + fmul(ratio_im, ratio_im)) as u128 - ).unwrap() as i128; - let ln_ratio_mod = if ratio_mod > 0 { ln_fixed_i(ratio_mod as u128).unwrap() } else { 0 }; + fp_div_i(fmul(2 * d.im, p_re) - fmul(2 * d.re, p_im), p_mod2).unwrap() + } else { + 0 + }; + let ratio_mod = + fp_sqrt((fmul(ratio_re, ratio_re) + fmul(ratio_im, ratio_im)) as u128).unwrap() as i128; + let ln_ratio_mod = if ratio_mod > 0 { + ln_fixed_i(ratio_mod as u128).unwrap() + } else { + 0 + }; let arg_ratio = atan2_fixed(ratio_im, ratio_re).unwrap(); - let real_exp = fmul(d_coeff_re, v0_i) - + fmul(mu, fmul(mm_re, t_i)) - + fmul(two_mu, ln_ratio_mod); - let imag_exp = fmul(d_coeff_im, v0_i) - + fmul(mu, fmul(mm_im, t_i)) - + fmul(two_mu, arg_ratio); + let real_exp = + fmul(d_coeff_re, v0_i) + fmul(mu, fmul(mm_re, t_i)) + fmul(two_mu, ln_ratio_mod); + let imag_exp = + fmul(d_coeff_im, v0_i) + fmul(mu, fmul(mm_im, t_i)) + fmul(two_mu, arg_ratio); let total_angle = imag_exp + fmul(u, x); let final_mag = exp_fixed_i(real_exp).unwrap(); @@ -343,17 +540,27 @@ fn heston_price_cf_raw( // atan2 // ============================================================ -#[cfg(test)] +#[cfg(all(test, feature = "complex"))] /// atan2 with unwrap — den > 0 guaranteed (swap ensures den = max(|x|,|y|) > 0). pub(crate) fn atan2_fixed(y: i128, x: i128) -> Result { const PI_HALF: i128 = 1_570_796_326_795; - if x == 0 && y == 0 { return Ok(0); } - if x == 0 { return Ok(if y > 0 { PI_HALF } else { -PI_HALF }); } - if y == 0 { return Ok(if x > 0 { 0 } else { PI_SCALE }); } + if x == 0 && y == 0 { + return Ok(0); + } + if x == 0 { + return Ok(if y > 0 { PI_HALF } else { -PI_HALF }); + } + if y == 0 { + return Ok(if x > 0 { 0 } else { PI_SCALE }); + } let ax = x.unsigned_abs(); let ay = y.unsigned_abs(); let swap = ay > ax; - let (num, den) = if swap { (ax as i128, ay as i128) } else { (ay as i128, ax as i128) }; + let (num, den) = if swap { + (ax as i128, ay as i128) + } else { + (ay as i128, ax as i128) + }; let z = fp_div_i(num, den).unwrap(); // den > 0: max(|x|,|y|) let a = atan_01(z); let a = if swap { PI_HALF - a } else { a }; @@ -361,14 +568,15 @@ pub(crate) fn atan2_fixed(y: i128, x: i128) -> Result { Ok(if y < 0 { -a } else { a }) } -#[cfg(test)] +#[cfg(all(test, feature = "complex"))] fn atan_01(z: i128) -> i128 { const TAN15: i128 = 267_949_192_431; const TAN30: i128 = 577_350_269_190; const PI_6: i128 = 523_598_775_598; const PI_4: i128 = 785_398_163_397; - if z <= TAN15 { atan_poly(z) } - else if z <= 750_000_000_000 { + if z <= TAN15 { + atan_poly(z) + } else if z <= 750_000_000_000 { // den = SCALE + z*TAN30 > SCALE (z,TAN30 > 0) PI_6 + atan_poly(fp_div_i(z - TAN30, SCALE_I + fp_mul_i(z, TAN30).unwrap()).unwrap()) } else { @@ -377,7 +585,7 @@ fn atan_01(z: i128) -> i128 { } } -#[cfg(test)] +#[cfg(all(test, feature = "complex"))] fn atan_poly(t: i128) -> i128 { let t2 = fp_mul_i(t, t).unwrap(); let p = fp_mul_i(t2, -90_909_090_909).unwrap() + 111_111_111_111; @@ -392,50 +600,226 @@ fn atan_poly(t: i128) -> i128 { // CIR helpers // ============================================================ -pub(crate) fn cir_rms_vol(v0: u128, kappa: u128, theta: u128, t: u128) -> Result { - Ok(fp_sqrt(cir_expected_var(v0, kappa, theta, t)?)?) +pub(crate) fn cir_rms_vol( + v0: u128, + kappa: u128, + theta: u128, + t: u128, +) -> Result { + let average_wide = cir_expected_var_wide(v0, kappa, theta, t)?; + if average_wide == 0 { + return Ok(0); + } + // average_wide is scaled by SCALE², so sqrt(average_wide) is sigma at SCALE. + let floor = isqrt_u128(average_wide); + let remainder = average_wide - floor * floor; + Ok(if remainder > floor { floor + 1 } else { floor }) +} + +#[cfg(test)] +pub(crate) fn cir_expected_var( + v0: u128, + kappa: u128, + theta: u128, + t: u128, +) -> Result { + let wide = cir_expected_var_wide(v0, kappa, theta, t)?; + wide.checked_add(SCALE / 2) + .map(|v| v / SCALE) + .ok_or(SolMathError::Overflow) +} + +const VAR_WIDE_SCALE: u128 = SCALE * SCALE; + +fn cir_expected_var_wide( + v0: u128, + kappa: u128, + theta: u128, + t: u128, +) -> Result { + if t == 0 || kappa == 0 { + return v0.checked_mul(SCALE).ok_or(SolMathError::Overflow); + } + // kappa and t are at SCALE, so their raw product represents kappa*T at + // SCALE² exactly. Public bounds keep this below 2e27, well inside i128. + let x_wide = kappa.checked_mul(t).ok_or(SolMathError::Overflow)?; + if x_wide == 0 { + return v0.checked_mul(SCALE).ok_or(SolMathError::Overflow); + } + + // Average variance = v0 + (theta-v0)*g(x), + // g(x) = 1 - (1-exp(-x))/x. This form avoids the catastrophic + // theta + (v0-theta)*ratio cancellation when x is small. + let g_wide = if x_wide <= VAR_WIDE_SCALE / 10 { + one_minus_exp_ratio_taylor_wide(x_wide)? + } else { + const WIDE_TO_HP: u128 = VAR_WIDE_SCALE / SCALE_HP as u128; + let x_hp_u = x_wide + .checked_add(WIDE_TO_HP / 2) + .ok_or(SolMathError::Overflow)? + / WIDE_TO_HP; + if x_hp_u > i128::MAX as u128 { + return Err(SolMathError::Overflow); + } + let x_hp = x_hp_u as i128; + let exp_neg = exp_fixed_hp(x_hp.checked_neg().ok_or(SolMathError::Overflow)?)?; + let one_minus_exp = SCALE_HP + .checked_sub(exp_neg) + .ok_or(SolMathError::Overflow)?; + let ratio = fp_div_hp_safe(one_minus_exp, x_hp)?; + let g_hp = SCALE_HP.checked_sub(ratio).ok_or(SolMathError::Overflow)?; + if g_hp < 0 || g_hp > SCALE_HP { + return Err(SolMathError::NoConvergence); + } + (g_hp as u128) + .checked_mul(WIDE_TO_HP) + .ok_or(SolMathError::Overflow)? + }; + + let v0_wide = v0.checked_mul(SCALE).ok_or(SolMathError::Overflow)?; + let theta_wide = theta.checked_mul(SCALE).ok_or(SolMathError::Overflow)?; + if v0_wide > i128::MAX as u128 || theta_wide > i128::MAX as u128 { + return Err(SolMathError::Overflow); + } + let delta_wide = (theta_wide as i128) + .checked_sub(v0_wide as i128) + .ok_or(SolMathError::Overflow)?; + let correction = mul_wide_round_i(delta_wide, g_wide as i128)?; + let average = (v0_wide as i128) + .checked_add(correction) + .ok_or(SolMathError::Overflow)?; + if average < 0 || average > VAR_WIDE_SCALE as i128 * 4 { + return Err(SolMathError::NoConvergence); + } + Ok(average as u128) +} + +/// Cancellation-safe Taylor series for +/// `1 - (1-exp(-x))/x = x/2 - x²/6 + x³/24 - ...` at SCALE². +fn one_minus_exp_ratio_taylor_wide(x: u128) -> Result { + debug_assert!(x > 0 && x <= VAR_WIDE_SCALE / 10); + const DENOMINATORS: [u128; 14] = [ + 2, + 6, + 24, + 120, + 720, + 5_040, + 40_320, + 362_880, + 3_628_800, + 39_916_800, + 479_001_600, + 6_227_020_800, + 87_178_291_200, + 1_307_674_368_000, + ]; + let mut power = x; + let mut sum = 0i128; + let mut i = 0usize; + while i < DENOMINATORS.len() { + let d = DENOMINATORS[i]; + let term = power.checked_add(d / 2).ok_or(SolMathError::Overflow)? / d; + if term > i128::MAX as u128 { + return Err(SolMathError::Overflow); + } + sum = if i % 2 == 0 { + sum.checked_add(term as i128) + } else { + sum.checked_sub(term as i128) + } + .ok_or(SolMathError::Overflow)?; + power = mul_wide_round_u(power, x)?; + i += 1; + } + if sum < 0 { + return Err(SolMathError::NoConvergence); + } + Ok(sum as u128) } -pub(crate) fn cir_expected_var(v0: u128, kappa: u128, theta: u128, t: u128) -> Result { - if t == 0 || kappa == 0 { return Ok(v0); } - let kappa_t = fp_mul(kappa, t)?; - // v0 and theta are variances at SCALE (≤ ~1*SCALE for typical vol ≤ 100%); - // both fit i128 trivially; difference ∈ (-SCALE_I, SCALE_I). No overflow. - let delta_i = v0 as i128 - theta as i128; - let em1 = expm1_fixed(-(kappa_t as i128))?; - let ratio = fp_div_i(-em1, kappa_t as i128)?; - // theta as i128 ≤ SCALE_I; fp_mul_i(delta_i, ratio) ≤ SCALE_I; sum ≤ 2·SCALE_I. Fits i128. - let result = theta as i128 + fp_mul_i(delta_i, ratio)?; - Ok(if result > 0 { result as u128 } else { 0 }) +fn mul_wide_round_u(a: u128, b: u128) -> Result { + let (q, rem) = checked_mul_div_rem_u(a, b, VAR_WIDE_SCALE).ok_or(SolMathError::Overflow)?; + if rem >= VAR_WIDE_SCALE - rem { + q.checked_add(1).ok_or(SolMathError::Overflow) + } else { + Ok(q) + } +} + +fn mul_wide_round_i(a: i128, b: i128) -> Result { + let neg = (a < 0) != (b < 0); + let mag = mul_wide_round_u(a.unsigned_abs(), b.unsigned_abs())?; + if neg { + if mag == 1u128 << 127 { + Ok(i128::MIN) + } else if mag < 1u128 << 127 { + Ok(-(mag as i128)) + } else { + Err(SolMathError::Overflow) + } + } else if mag <= i128::MAX as u128 { + Ok(mag as i128) + } else { + Err(SolMathError::Overflow) + } } // ============================================================ // DE quadrature: h=0.25, 21 nodes // ============================================================ -#[cfg(test)] +#[cfg(all(test, feature = "complex"))] const DE_N: usize = 21; -#[cfg(test)] +#[cfg(all(test, feature = "complex"))] const DE_NODES: [i128; 21] = [ - 146_529, 4_855_077, 74_579_941, - 630_580_368, 3_355_820_405, 12_485_683_179, - 35_272_052_349, 80_758_778_787,157_867_103_304, - 274_805_390_708,441_077_539_800,672_466_825_156, - 1_000_000_000_000,1_487_062_205_288,2_267_175_065_076, - 3_638_938_804_750,6_334_441_939_257,12_382_554_751_561, - 28_351_057_945_605,80_091_732_721_767,297_989_725_118_823, + 146_529, + 4_855_077, + 74_579_941, + 630_580_368, + 3_355_820_405, + 12_485_683_179, + 35_272_052_349, + 80_758_778_787, + 157_867_103_304, + 274_805_390_708, + 441_077_539_800, + 672_466_825_156, + 1_000_000_000_000, + 1_487_062_205_288, + 2_267_175_065_076, + 3_638_938_804_750, + 6_334_441_939_257, + 12_382_554_751_561, + 28_351_057_945_605, + 80_091_732_721_767, + 297_989_725_118_823, ]; -#[cfg(test)] +#[cfg(all(test, feature = "complex"))] const DE_WEIGHTS: [i128; 21] = [ - 579_312, 14_972_941, 179_599_271, - 1_187_766_015, 4_957_925_286, 14_533_760_073, - 32_583_937_342, 59_889_282_729, 95_662_152_103, - 139_716_814_232,195_316_933_331,272_372_585_178, - 392_699_081_699,602_312_206_376,1_003_945_204_820, - 1_850_112_676_736,3_838_458_650_317,9_182_683_710_144, - 26_190_398_181_291,93_229_502_186_530,440_253_236_111_365, + 579_312, + 14_972_941, + 179_599_271, + 1_187_766_015, + 4_957_925_286, + 14_533_760_073, + 32_583_937_342, + 59_889_282_729, + 95_662_152_103, + 139_716_814_232, + 195_316_933_331, + 272_372_585_178, + 392_699_081_699, + 602_312_206_376, + 1_003_945_204_820, + 1_850_112_676_736, + 3_838_458_650_317, + 9_182_683_710_144, + 26_190_398_181_291, + 93_229_502_186_530, + 440_253_236_111_365, ]; // ============================================================ @@ -450,55 +834,189 @@ pub(crate) mod tests { #[test] fn test_heston_xi_zero_bs_path() { - let (c, p) = heston_price(100*SCALE, 100*SCALE, 50_000_000_000, SCALE, - 40_000_000_000, 2*SCALE, 40_000_000_000, 0, -700_000_000_000).unwrap(); - let sigma = cir_rms_vol(40_000_000_000, 2*SCALE, 40_000_000_000, SCALE).unwrap(); - let bs = bs_full_hp(100*SCALE, 100*SCALE, 50_000_000_000, sigma, SCALE).unwrap(); + let (c, p) = heston_price( + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + SCALE, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + 0, + -700_000_000_000, + ) + .unwrap(); + let sigma = cir_rms_vol(40_000_000_000, 2 * SCALE, 40_000_000_000, SCALE).unwrap(); + let bs = bs_full_hp(100 * SCALE, 100 * SCALE, 50_000_000_000, sigma, SCALE).unwrap(); assert_eq!(c, bs.call); - assert_eq!(p, bs.put); + let kd = discounted_strike(100 * SCALE, 50_000_000_000, SCALE).unwrap(); + assert_eq!(c as i128 - p as i128, 100 * SCALE_I - kd as i128); } #[test] - fn test_heston_small_xi_bs_path() { - let (c, _) = heston_price(100*SCALE, 100*SCALE, 0, 100_000_000_000, - 10_000_000_000, 500_000_000_000, 10_000_000_000, 100_000_000_000, -900_000_000_000).unwrap(); - let sigma = cir_rms_vol(10_000_000_000, 500_000_000_000, 10_000_000_000, 100_000_000_000).unwrap(); - let bs = bs_full_hp(100*SCALE, 100*SCALE, 0, sigma, 100_000_000_000).unwrap(); - assert_eq!(c, bs.call); + fn test_heston_fails_closed_on_material_arbitrage_violation() { + let result = heston_price( + 50 * SCALE, + 50 * SCALE, + 50_000_000_000, + SCALE, + 1_000_000_000, + 0, + 0, + 100_000_000_000, + 700_000_000_000, + ); + assert_eq!(result, Err(SolMathError::NoConvergence)); + } + + #[test] + fn test_heston_rejects_unproved_i64_node_domain() { + assert_eq!( + heston_price( + 100 * SCALE, + 100 * SCALE, + 0, + SCALE, + 40_000_000_000, + 2_000_000 * SCALE, + 0, + 2_000_000 * SCALE, + -999_000_000_000, + ), + Err(SolMathError::DomainError) + ); + } + + #[test] + fn test_heston_zero_variance_is_discounted_intrinsic() { + let (call, put) = heston_price( + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + SCALE, + 0, + 2 * SCALE, + 0, + 0, + -700_000_000_000, + ) + .unwrap(); + let kd = discounted_strike(100 * SCALE, 50_000_000_000, SCALE).unwrap(); + assert_eq!((call, put), (100 * SCALE - kd, 0)); } #[test] - fn test_heston_cf_path_differs() { - let (c, _) = heston_price(100*SCALE, 100*SCALE, 50_000_000_000, SCALE, - 40_000_000_000, 2*SCALE, 40_000_000_000, 500_000_000_000, -700_000_000_000).unwrap(); - let sigma = cir_rms_vol(40_000_000_000, 2*SCALE, 40_000_000_000, SCALE).unwrap(); - let bs = bs_full_hp(100*SCALE, 100*SCALE, 50_000_000_000, sigma, SCALE).unwrap(); + fn test_all_positive_stochastic_xi_fails_closed() { + for xi in [1, 100_000_000_000, 500_000_000_000] { + assert_eq!( + heston_price( + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + SCALE, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + xi, + -700_000_000_000, + ), + Err(SolMathError::NoConvergence) + ); + } + } + + #[test] + fn test_heston_small_positive_xi_is_not_approximated() { + assert_eq!( + heston_price( + 100 * SCALE, + 100 * SCALE, + 0, + 100_000_000_000, + 10_000_000_000, + 500_000_000_000, + 10_000_000_000, + 1, + -900_000_000_000, + ), + Err(SolMathError::NoConvergence) + ); + } + + #[test] + #[cfg(feature = "complex")] + fn test_research_cf_path_differs_from_deterministic_reduction() { + let (c, _) = heston_price_cv( + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + SCALE, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + 500_000_000_000, + -700_000_000_000, + ) + .unwrap(); + let sigma = cir_rms_vol(40_000_000_000, 2 * SCALE, 40_000_000_000, SCALE).unwrap(); + let bs = bs_full_hp(100 * SCALE, 100 * SCALE, 50_000_000_000, sigma, SCALE).unwrap(); assert_ne!(c, bs.call); } #[test] fn test_heston_put_call_parity() { let r: u128 = 50_000_000_000; - let (c, p) = heston_price(100*SCALE, 100*SCALE, r, SCALE, - 40_000_000_000, 2*SCALE, 40_000_000_000, 500_000_000_000, -700_000_000_000).unwrap(); + let (c, p) = heston_price( + 100 * SCALE, + 100 * SCALE, + r, + SCALE, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + 0, + -700_000_000_000, + ) + .unwrap(); let disc = exp_fixed_i(-fp_mul_i(r as i128, SCALE_I).unwrap()).unwrap(); - let parity = (c as i128 - p as i128 - 100*SCALE_I + fp_mul_i(100*SCALE_I, disc).unwrap()).abs(); + let parity = + (c as i128 - p as i128 - 100 * SCALE_I + fp_mul_i(100 * SCALE_I, disc).unwrap()).abs(); assert!(parity <= 100, "parity error {}", parity); } #[test] fn test_heston_t_zero() { - let (c, p) = heston_price(110*SCALE, 100*SCALE, 0, 0, - 40_000_000_000, 2*SCALE, 40_000_000_000, 500_000_000_000, -700_000_000_000).unwrap(); - assert_eq!(c, 10*SCALE); + let (c, p) = heston_price( + 110 * SCALE, + 100 * SCALE, + 0, + 0, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + 500_000_000_000, + -700_000_000_000, + ) + .unwrap(); + assert_eq!(c, 10 * SCALE); assert_eq!(p, 0); } #[test] fn test_heston_all_moneyness() { for &(s, k) in &[(80, 100), (100, 100), (120, 100)] { - let (c, p) = heston_price(s as u128*SCALE, k as u128*SCALE, 50_000_000_000, SCALE, - 40_000_000_000, 2*SCALE, 40_000_000_000, 400_000_000_000, -600_000_000_000).unwrap(); + let (c, p) = heston_price( + s as u128 * SCALE, + k as u128 * SCALE, + 50_000_000_000, + SCALE, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + 0, + -600_000_000_000, + ) + .unwrap(); assert!(c > 0 || s <= k); assert!(p > 0 || s >= k); } @@ -507,9 +1025,16 @@ pub(crate) mod tests { // ── CV vs raw i128 comparison tests ── /// Helper: compare CV result against 21-node i128 raw Lewis. + #[cfg(feature = "complex")] fn compare_cv_vs_raw( - s: u128, k: u128, r: u128, t: u128, - v0: u128, kappa: u128, theta: u128, xi: u128, + s: u128, + k: u128, + r: u128, + t: u128, + v0: u128, + kappa: u128, + theta: u128, + xi: u128, rho: i128, label: &str, max_err: i128, @@ -522,135 +1047,236 @@ pub(crate) mod tests { // Also verify put-call parity for the CV result let disc = exp_fixed_i(-fp_mul_i(r as i128, t as i128).unwrap()).unwrap(); - let parity = (cv_c as i128 - cv_p as i128 - s as i128 + fp_mul_i(k as i128, disc).unwrap()).abs(); + let parity = + (cv_c as i128 - cv_p as i128 - s as i128 + fp_mul_i(k as i128, disc).unwrap()).abs(); - assert!(err_c <= max_err, + assert!( + err_c <= max_err, "{} call: cv={}, raw={}, err={} > max={}", - label, cv_c, raw_c, err_c, max_err); - assert!(err_p <= max_err, + label, + cv_c, + raw_c, + err_c, + max_err + ); + assert!( + err_p <= max_err, "{} put: cv={}, raw={}, err={} > max={}", - label, cv_p, raw_p, err_p, max_err); - assert!(parity <= 1000, + label, + cv_p, + raw_p, + err_p, + max_err + ); + assert!( + parity <= 1000, "{} put-call parity error: {} (c={}, p={}, s={}, k_disc={})", - label, parity, cv_c, cv_p, s, fp_mul_i(k as i128, disc).unwrap()); + label, + parity, + cv_c, + cv_p, + s, + fp_mul_i(k as i128, disc).unwrap() + ); } // $0.02 tolerance: 20_000_000_000 at SCALE on $100 notional. // Most cases achieve sub-penny ($0.01); deep ITM reaches ~$0.018 // due to inherent i64 CF precision for large forward moneyness. + #[cfg(feature = "complex")] const MAX_ERR: i128 = 20_000_000_000; #[test] + #[cfg(feature = "complex")] fn test_cv_vs_raw_atm() { compare_cv_vs_raw( - 100*SCALE, 100*SCALE, 50_000_000_000, SCALE, - 40_000_000_000, 2*SCALE, 40_000_000_000, 300_000_000_000, + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + SCALE, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + 300_000_000_000, -700_000_000_000, - "ATM ξ=0.3 ρ=-0.7", MAX_ERR, + "ATM ξ=0.3 ρ=-0.7", + MAX_ERR, ); } #[test] + #[cfg(feature = "complex")] fn test_cv_vs_raw_itm() { compare_cv_vs_raw( - 120*SCALE, 100*SCALE, 50_000_000_000, SCALE, - 40_000_000_000, 2*SCALE, 40_000_000_000, 300_000_000_000, + 120 * SCALE, + 100 * SCALE, + 50_000_000_000, + SCALE, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + 300_000_000_000, -700_000_000_000, - "ITM 120/100", MAX_ERR, + "ITM 120/100", + MAX_ERR, ); } #[test] + #[cfg(feature = "complex")] fn test_cv_vs_raw_otm() { compare_cv_vs_raw( - 80*SCALE, 100*SCALE, 50_000_000_000, SCALE, - 40_000_000_000, 2*SCALE, 40_000_000_000, 300_000_000_000, + 80 * SCALE, + 100 * SCALE, + 50_000_000_000, + SCALE, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + 300_000_000_000, -700_000_000_000, - "OTM 80/100", MAX_ERR, + "OTM 80/100", + MAX_ERR, ); } #[test] + #[cfg(feature = "complex")] fn test_cv_vs_raw_high_xi() { compare_cv_vs_raw( - 100*SCALE, 100*SCALE, 50_000_000_000, SCALE, - 40_000_000_000, 2*SCALE, 40_000_000_000, 500_000_000_000, + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + SCALE, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + 500_000_000_000, -700_000_000_000, - "ATM ξ=0.5", MAX_ERR, + "ATM ξ=0.5", + MAX_ERR, ); } #[test] + #[cfg(feature = "complex")] fn test_cv_vs_raw_very_high_xi() { compare_cv_vs_raw( - 100*SCALE, 100*SCALE, 50_000_000_000, SCALE, - 40_000_000_000, 2*SCALE, 40_000_000_000, 800_000_000_000, + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + SCALE, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + 800_000_000_000, -700_000_000_000, - "ATM ξ=0.8", MAX_ERR, + "ATM ξ=0.8", + MAX_ERR, ); } #[test] + #[cfg(feature = "complex")] fn test_cv_vs_raw_zero_rho() { compare_cv_vs_raw( - 100*SCALE, 100*SCALE, 50_000_000_000, SCALE, - 40_000_000_000, 2*SCALE, 40_000_000_000, 300_000_000_000, + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + SCALE, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + 300_000_000_000, 0, - "ATM ρ=0", MAX_ERR, + "ATM ρ=0", + MAX_ERR, ); } #[test] + #[cfg(feature = "complex")] fn test_cv_vs_raw_short_maturity() { compare_cv_vs_raw( - 100*SCALE, 100*SCALE, 50_000_000_000, 100_000_000_000, - 40_000_000_000, 2*SCALE, 40_000_000_000, 300_000_000_000, + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + 100_000_000_000, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + 300_000_000_000, -700_000_000_000, - "ATM T=0.1", MAX_ERR, + "ATM T=0.1", + MAX_ERR, ); } #[test] + #[cfg(feature = "complex")] fn test_cv_vs_raw_long_maturity() { compare_cv_vs_raw( - 100*SCALE, 100*SCALE, 50_000_000_000, 2*SCALE, - 40_000_000_000, 2*SCALE, 40_000_000_000, 300_000_000_000, + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + 2 * SCALE, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + 300_000_000_000, -700_000_000_000, - "ATM T=2.0", MAX_ERR, + "ATM T=2.0", + MAX_ERR, ); } #[test] + #[cfg(feature = "complex")] fn test_cv_vs_raw_deep_otm_high_xi() { compare_cv_vs_raw( - 80*SCALE, 100*SCALE, 50_000_000_000, SCALE, - 40_000_000_000, 2*SCALE, 40_000_000_000, 500_000_000_000, + 80 * SCALE, + 100 * SCALE, + 50_000_000_000, + SCALE, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + 500_000_000_000, -500_000_000_000, - "OTM 80/100 ξ=0.5 ρ=-0.5", MAX_ERR, + "OTM 80/100 ξ=0.5 ρ=-0.5", + MAX_ERR, ); } #[test] + #[cfg(feature = "complex")] fn test_cv_vs_raw_negative_rho_strong() { compare_cv_vs_raw( - 100*SCALE, 100*SCALE, 50_000_000_000, SCALE, - 40_000_000_000, 2*SCALE, 40_000_000_000, 300_000_000_000, + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + SCALE, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + 300_000_000_000, -900_000_000_000, - "ATM ρ=-0.9", MAX_ERR, + "ATM ρ=-0.9", + MAX_ERR, ); } /// Full matrix test: moneyness × maturity × xi × rho #[test] + #[cfg(feature = "complex")] fn test_cv_vs_raw_full_matrix() { - let moneyness: [(u128, u128); 5] = [ - (80, 100), (90, 100), (100, 100), (110, 100), (120, 100), - ]; + let moneyness: [(u128, u128); 5] = + [(80, 100), (90, 100), (100, 100), (110, 100), (120, 100)]; let maturities: [u128; 4] = [ 100_000_000_000, // T=0.1 250_000_000_000, // T=0.25 SCALE, // T=1.0 - 2*SCALE, // T=2.0 + 2 * SCALE, // T=2.0 ]; let xis: [u128; 3] = [ 200_000_000_000, // ξ=0.2 @@ -673,20 +1299,49 @@ pub(crate) mod tests { let s = s_mult * SCALE; let k = k_mult * SCALE; - let cv = heston_price_cv(s, k, 50_000_000_000, t, - 40_000_000_000, 2*SCALE, 40_000_000_000, xi, rho); - let raw = heston_price_cf_raw(s, k, 50_000_000_000, t, - 40_000_000_000, 2*SCALE, 40_000_000_000, xi, rho); + let cv = heston_price_cv( + s, + k, + 50_000_000_000, + t, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + xi, + rho, + ); + let raw = heston_price_cf_raw( + s, + k, + 50_000_000_000, + t, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + xi, + rho, + ); if let (Ok((cv_c, _)), Ok((raw_c, _))) = (cv, raw) { let err = (cv_c as i128 - raw_c as i128).abs(); - if err > max_err_seen { max_err_seen = err; } + if err > max_err_seen { + max_err_seen = err; + } count += 1; // $2.00 tolerance on $100 notional = 2% of notional - assert!(err <= 2_000_000_000_000, + assert!( + err <= 2_000_000_000_000, "S={} K={} T={} ξ={} ρ={}: cv={} raw={} err={}", - s_mult, k_mult, t, xi, rho, cv_c, raw_c, err); + s_mult, + k_mult, + t, + xi, + rho, + cv_c, + raw_c, + err + ); } } } @@ -697,9 +1352,9 @@ pub(crate) mod tests { assert!(count >= 150, "Only {} test cases ran", count); } - /// Put-call parity across the full matrix + /// Public stochastic pricing must remain disabled across the old matrix. #[test] - fn test_cv_put_call_parity_matrix() { + fn test_public_stochastic_matrix_fails_closed() { let params: [(u128, u128, u128, i128); 6] = [ (100, 100, 300_000_000_000, -700_000_000_000), (80, 100, 300_000_000_000, -500_000_000_000), @@ -708,19 +1363,23 @@ pub(crate) mod tests { (90, 100, 800_000_000_000, -700_000_000_000), (110, 100, 300_000_000_000, -900_000_000_000), ]; - let r: u128 = 50_000_000_000; - let t: u128 = SCALE; - for &(s_mult, k_mult, xi, rho) in ¶ms { let s = s_mult * SCALE; let k = k_mult * SCALE; - let (c, p) = heston_price(s, k, r, t, - 40_000_000_000, 2*SCALE, 40_000_000_000, xi, rho).unwrap(); - let disc = exp_fixed_i(-fp_mul_i(r as i128, t as i128).unwrap()).unwrap(); - let parity = (c as i128 - p as i128 - s as i128 + fp_mul_i(k as i128, disc).unwrap()).abs(); - assert!(parity <= 1000, - "PCP fail S={} K={} ξ={} ρ={}: C={} P={} err={}", - s_mult, k_mult, xi, rho, c, p, parity); + assert_eq!( + heston_price( + s, + k, + 50_000_000_000, + SCALE, + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + xi, + rho, + ), + Err(SolMathError::NoConvergence) + ); } } @@ -729,23 +1388,55 @@ pub(crate) mod tests { let s = 100 * SCALE; let r = 50_000_000_000u128; let params: &[(u128, u128, u128, u128, i128)] = &[ - (40_000_000_000, 2*SCALE, 40_000_000_000, 300_000_000_000, -700_000_000_000), - (40_000_000_000, 2*SCALE, 40_000_000_000, 800_000_000_000, -500_000_000_000), + ( + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + 300_000_000_000, + -700_000_000_000, + ), + ( + 40_000_000_000, + 2 * SCALE, + 40_000_000_000, + 800_000_000_000, + -500_000_000_000, + ), ]; for &(v0, kappa, theta, xi, rho) in params { for &k_mult in &[80u128, 100, 120] { let k = k_mult * SCALE; - for &t in &[SCALE/10, SCALE, 2*SCALE] { - if let Ok((call, put)) = heston_price(s, k, r, t, v0, kappa, theta, xi, rho) { - assert!(call <= s, "Call {} > spot {}", call, s); - assert!(put <= k, "Put {} > strike {}", put, k); - } + for &t in &[SCALE / 10, SCALE, 2 * SCALE] { + assert_eq!( + heston_price(s, k, r, t, v0, kappa, theta, xi, rho), + Err(SolMathError::NoConvergence) + ); } } } } + #[test] + fn test_cancellation_safe_deterministic_variance_reproducer() { + let average = cir_expected_var(0, 1_000_000, 4 * SCALE, SCALE).unwrap(); + assert_eq!(average, 1_999_999); + let (call, put) = heston_price( + 100 * SCALE, + 100 * SCALE, + 0, + SCALE, + 0, + 1_000_000, + 4 * SCALE, + 0, + 0, + ) + .unwrap(); + assert_eq!(call, 56_418_944_263); + assert_eq!(put, call); + } } -#[cfg(test)] -include!("../test_data/heston_reference_tests.rs"); +// The committed QuantLib stochastic corpus is intentionally not included in +// the release test suite: every vector has xi > 0 and the public API rejects +// it. The file remains available for offline research on the retained CV code. diff --git a/src/hp.rs b/src/hp.rs index 614683f..f624f0d 100644 --- a/src/hp.rs +++ b/src/hp.rs @@ -1,11 +1,10 @@ +use crate::arithmetic::{fp_mul, fp_mul_i, fp_sqrt, isqrt_u128}; use crate::constants::*; use crate::double_word::DoubleWord; use crate::error::SolMathError; -use crate::arithmetic::{fp_mul, fp_mul_i, fp_sqrt, isqrt_u128}; use crate::overflow::{checked_mul_div_i, checked_mul_div_u}; use crate::transcendental::exp_fixed_i; - /// Unsigned high-precision fixed-point multiply at `SCALE_HP` (1e15). /// /// - **a**, **b**: unsigned fixed-point at 1e15 scale. @@ -19,16 +18,26 @@ pub fn fp_mul_hp_u(a: u128, b: u128) -> Result { let hi_b = b / SCALE_HP_U; let lo_b = b % SCALE_HP_U; - let hh = hi_a.checked_mul(hi_b).ok_or(SolMathError::Overflow)? - .checked_mul(SCALE_HP_U).ok_or(SolMathError::Overflow)?; + let hh = hi_a + .checked_mul(hi_b) + .ok_or(SolMathError::Overflow)? + .checked_mul(SCALE_HP_U) + .ok_or(SolMathError::Overflow)?; let hl = hi_a.checked_mul(lo_b).ok_or(SolMathError::Overflow)?; let lh = lo_a.checked_mul(hi_b).ok_or(SolMathError::Overflow)?; - let ll = lo_a.checked_mul(lo_b).ok_or(SolMathError::Overflow)? - .checked_add(SCALE_HP_U / 2).ok_or(SolMathError::Overflow)? / SCALE_HP_U; - - hh.checked_add(hl).ok_or(SolMathError::Overflow)? - .checked_add(lh).ok_or(SolMathError::Overflow)? - .checked_add(ll).ok_or(SolMathError::Overflow) + let ll = lo_a + .checked_mul(lo_b) + .ok_or(SolMathError::Overflow)? + .checked_add(SCALE_HP_U / 2) + .ok_or(SolMathError::Overflow)? + / SCALE_HP_U; + + hh.checked_add(hl) + .ok_or(SolMathError::Overflow)? + .checked_add(lh) + .ok_or(SolMathError::Overflow)? + .checked_add(ll) + .ok_or(SolMathError::Overflow) } /// Signed high-precision fixed-point multiply at `SCALE_HP` (1e15). @@ -74,12 +83,15 @@ pub(crate) fn fp_mul_hp_fast(a: i128, b: i128) -> i128 { /// - **b**: signed denominator at 1e15 scale. Must be non-zero. /// - **Returns**: `i128` at 1e15 scale. /// - **Errors**: `DivisionByZero` if `b == 0`, `Overflow` if the scaled quotient exceeds `i128`. -/// - **Accuracy**: exact to rounding. +/// - **Rounding**: exact truncation toward zero. #[inline] pub fn fp_div_hp_safe(a: i128, b: i128) -> Result { if b == 0 { return Err(SolMathError::DivisionByZero); } + if a == i128::MIN && b == -1 { + return Err(SolMathError::Overflow); + } let q = a / b; let r = a % b; @@ -164,8 +176,7 @@ pub fn ln_fixed_hp(x: i128) -> Result { let q = p / t_den; let r = p % t_den; // t_den ∈ (SCALE_HP, 3·SCALE_HP), half_den ≤ 1.5·SCALE_HP, fits i128. - let half_den = t_den / 2; - let (t, t_lo) = if r >= half_den { + let (t, t_lo) = if r >= t_den - r { // r - t_den ∈ (-t_den, 0); (r - t_den) * SCALE_HP: |product| < 3e30, fits i128. // r * SCALE_HP: r < t_den < 3e15, product < 3e30, fits i128. (q + 1, (r - t_den) * SCALE_HP / t_den) @@ -230,9 +241,15 @@ pub fn ln_fixed_hp(x: i128) -> Result { pub fn exp_fixed_hp(x: i128) -> Result { let max_x = 40 * SCALE_HP; - if x <= -max_x { return Ok(0); } - if x >= max_x { return Err(SolMathError::Overflow); } - if x == 0 { return Ok(SCALE_HP); } + if x <= -max_x { + return Ok(0); + } + if x >= max_x { + return Err(SolMathError::Overflow); + } + if x == 0 { + return Ok(SCALE_HP); + } // Split LN2: LN2_HP undershoots (LN2_HP_LO > 0), so k*LN2_HP is too small // and r = x - k*LN2_HP is too large. Subtract the positive correction. @@ -241,14 +258,23 @@ pub fn exp_fixed_hp(x: i128) -> Result { let ln2_correction = { // k ∈ (-58, 58), LN2_HP_LO ≈ 3e14; k * LN2_HP_LO ≤ 58 * 3e14 ≈ 1.7e16, fits i128. let raw = k * LN2_HP_LO; - if raw >= 0 { (raw + SCALE_HP / 2) / SCALE_HP } else { (raw - SCALE_HP / 2) / SCALE_HP } + if raw >= 0 { + (raw + SCALE_HP / 2) / SCALE_HP + } else { + (raw - SCALE_HP / 2) / SCALE_HP + } }; // k * LN2_HP ≤ 58 * 6.9e14 ≈ 4e16; x ≤ 40·SCALE_HP = 4e16; r ∈ (-2·SCALE_HP, 2·SCALE_HP), fits i128. let mut r = x - k * LN2_HP - ln2_correction; // r ∈ (-2·SCALE_HP, 2·SCALE_HP); LN2_HP ≈ 6.9e14; r ± LN2_HP ∈ (-3·SCALE_HP, 3·SCALE_HP), fits i128. - if r > HALF_LN2_HP { k += 1; r -= LN2_HP; } - else if r < -HALF_LN2_HP { k -= 1; r += LN2_HP; } + if r > HALF_LN2_HP { + k += 1; + r -= LN2_HP; + } else if r < -HALF_LN2_HP { + k -= 1; + r += LN2_HP; + } let xx = fp_mul_hp_fast(r, r); @@ -310,41 +336,44 @@ pub fn pow_fixed_hp(base: u128, exponent: u128) -> Result { } let base_hp = upscale_std_to_hp(base)?; - let exp_hp = upscale_std_to_hp(exponent)?; + let exp_hp = match upscale_std_to_hp(exponent) { + Ok(v) => v, + Err(SolMathError::Overflow) if base < SCALE => return Ok(0), + Err(e) => return Err(e), + }; let ln_base = ln_fixed_hp(base_hp)?; - let product = fp_mul_hp_i(exp_hp, ln_base)?; + let product = match fp_mul_hp_i(exp_hp, ln_base) { + Ok(v) => v, + Err(SolMathError::Overflow) if base < SCALE => return Ok(0), + Err(e) => return Err(e), + }; // Fast path: product fits in exp_fixed_hp's range - if product.abs() < 39 * SCALE_HP { + if product.unsigned_abs() < (39 * SCALE_HP) as u128 { let result_hp = exp_fixed_hp(product)?; return Ok(downscale_hp_to_std(result_hp)); } // Split path: decompose exponent into integer + fractional parts. - let n = (exponent / SCALE) as u32; let frac_std = exponent % SCALE; let mut int_result: u128 = SCALE; let mut pow_base: u128 = base; - let mut remaining = n; + let mut remaining = exponent / SCALE; while remaining > 0 { if remaining & 1 == 1 { int_result = match checked_mul_div_u(int_result, pow_base, SCALE) { - Some(v) if v > 0 => v, - _ => return Ok(0), + Some(0) => return Ok(0), + Some(v) => v, + None => return Err(SolMathError::Overflow), }; } remaining >>= 1; if remaining > 0 { pow_base = match checked_mul_div_u(pow_base, pow_base, SCALE) { - Some(v) if v > 0 => v, - _ => { - if base < SCALE { - return Ok(0); - } else { - return Err(SolMathError::Overflow); - } - } + Some(0) => return Ok(0), + Some(v) => v, + None => return Err(SolMathError::Overflow), }; } } @@ -360,13 +389,7 @@ pub fn pow_fixed_hp(base: u128, exponent: u128) -> Result { match checked_mul_div_u(int_result, frac_result, SCALE) { Some(v) => Ok(v), - None => { - if base < SCALE { - Ok(0) - } else { - Err(SolMathError::Overflow) - } - } + None => Err(SolMathError::Overflow), } } @@ -556,17 +579,15 @@ pub fn norm_cdf_poly_hp(x: i128) -> Result { let cdf_pos = cdf_pos.clamp(0, SCALE_HP); // cdf_pos ∈ [0, SCALE_HP] after clamp; SCALE_HP - cdf_pos ∈ [0, SCALE_HP], fits i128. - Ok(if x >= 0 { - cdf_pos - } else { - SCALE_HP - cdf_pos - }) + Ok(if x >= 0 { cdf_pos } else { SCALE_HP - cdf_pos }) } /// Map |x| to local Chebyshev variable t at HP scale. Internal — called by norm_cdf_poly_hp. #[inline] pub(crate) fn poly_map_t_hp(ax: i128, mid: i128, hw: i128) -> Result { - let product = (ax - mid).checked_mul(SCALE_HP).ok_or(SolMathError::Overflow)?; + let product = (ax - mid) + .checked_mul(SCALE_HP) + .ok_or(SolMathError::Overflow)?; Ok(product / hw) } @@ -580,7 +601,6 @@ pub(crate) struct BsIntermediatesHp { pub d2_hp: i128, pub phi_d1_hp: i128, pub phi_d2_hp: i128, - pub phi_neg_d1_hp: i128, pub phi_neg_d2_hp: i128, pub k_disc_hp: i128, pub sigma_sqrt_t_hp: i128, @@ -592,7 +612,11 @@ pub(crate) struct BsIntermediatesHp { /// Compute HP Black-Scholes intermediates shared between price-only and full Greeks. pub(crate) fn compute_bs_intermediates_hp( - s: u128, k: u128, r: u128, sigma: u128, t: u128, + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, ) -> Result { let s_hp = upscale_std_to_hp(s)?; let k_hp = upscale_std_to_hp(k)?; @@ -600,7 +624,11 @@ pub(crate) fn compute_bs_intermediates_hp( let sigma_hp = upscale_std_to_hp(sigma)?; let t_hp = upscale_std_to_hp(t)?; - let sqrt_t_hp = isqrt_u128((t_hp as u128).checked_mul(SCALE_HP_U).ok_or(SolMathError::Overflow)?) as i128; + let sqrt_t_hp = isqrt_u128( + (t_hp as u128) + .checked_mul(SCALE_HP_U) + .ok_or(SolMathError::Overflow)?, + ) as i128; let sigma_sqrt_t_hp = fp_mul_hp_i(sigma_hp, sqrt_t_hp)?; let sk_ratio_hp = fp_div_hp_safe(s_hp, k_hp)?; @@ -609,9 +637,14 @@ pub(crate) fn compute_bs_intermediates_hp( // sigma_sq_half_hp: fp_mul_hp_i is checked; /2: ∈ [0, SCALE_HP/2], fits i128. let sigma_sq_half_hp = fp_mul_hp_i(sigma_hp, sigma_hp)? / 2; // r_hp ∈ [0, SCALE_HP], sigma_sq_half_hp ∈ [0, SCALE_HP/2]: sum ≤ 1.5·SCALE_HP, fits i128. - let drift_hp = fp_mul_hp_i(r_hp + sigma_sq_half_hp, t_hp)?; + let drift_rate_hp = r_hp + .checked_add(sigma_sq_half_hp) + .ok_or(SolMathError::Overflow)?; + let drift_hp = fp_mul_hp_i(drift_rate_hp, t_hp)?; // ln_sk_hp ∈ [-40·SCALE_HP, 40·SCALE_HP], drift_hp ∈ [-SCALE_HP, SCALE_HP]; sum fits i128. - let d1_num_hp = ln_sk_hp + drift_hp; + let d1_num_hp = ln_sk_hp + .checked_add(drift_hp) + .ok_or(SolMathError::Overflow)?; let d1_hp = if sigma_sqrt_t_hp > 0 { fp_div_hp_safe(d1_num_hp, sigma_sqrt_t_hp)? @@ -620,12 +653,13 @@ pub(crate) fn compute_bs_intermediates_hp( }; // d1_hp ∈ [-8·SCALE_HP, 8·SCALE_HP] (clamped by norm_cdf_poly_hp); sigma_sqrt_t_hp ∈ [0, ~SCALE_HP]; // d2_hp = d1_hp - sigma_sqrt_t_hp ∈ [-9·SCALE_HP, 8·SCALE_HP], fits i128. - let d2_hp = d1_hp - sigma_sqrt_t_hp; + let d2_hp = d1_hp + .checked_sub(sigma_sqrt_t_hp) + .ok_or(SolMathError::Overflow)?; let phi_d1_hp = norm_cdf_poly_hp(d1_hp)?; let phi_d2_hp = norm_cdf_poly_hp(d2_hp)?; // phi_d1_hp, phi_d2_hp ∈ [0, SCALE_HP]; SCALE_HP - phi ∈ [0, SCALE_HP], fits i128. - let phi_neg_d1_hp = SCALE_HP - phi_d1_hp; let phi_neg_d2_hp = SCALE_HP - phi_d2_hp; let r_t_hp = fp_mul_hp_i(r_hp, t_hp)?; @@ -633,17 +667,26 @@ pub(crate) fn compute_bs_intermediates_hp( let k_disc_hp = fp_mul_hp_i(k_hp, discount_hp)?; Ok(BsIntermediatesHp { - s_hp, k_hp, d1_hp, d2_hp, - phi_d1_hp, phi_d2_hp, phi_neg_d1_hp, phi_neg_d2_hp, - k_disc_hp, sigma_sqrt_t_hp, sqrt_t_hp, - sigma_hp, r_hp, t_hp, + s_hp, + k_hp, + d1_hp, + d2_hp, + phi_d1_hp, + phi_d2_hp, + phi_neg_d2_hp, + k_disc_hp, + sigma_sqrt_t_hp, + sqrt_t_hp, + sigma_hp, + r_hp, + t_hp, }) } /// High-precision Black-Scholes call and put prices (no Greeks). /// /// Accepts and returns values at `SCALE` (1e12) but computes internally at `SCALE_HP` (1e15). -/// ~60K CU on Solana. +/// Final SBF audit: 84,528 CU average, 83,535 median, 122,380 max. /// /// - **s**: spot price at `SCALE`. /// - **k**: strike price at `SCALE`. @@ -654,10 +697,17 @@ pub(crate) fn compute_bs_intermediates_hp( /// - **Errors**: `DomainError` if `sigma == 0` or `t == 0`. /// - **Accuracy**: 3-4 ULP max. pub fn black_scholes_price_hp( - s: u128, k: u128, r: u128, sigma: u128, t: u128, + s: u128, + k: u128, + r: u128, + sigma: u128, + t: u128, ) -> Result<(u128, u128), SolMathError> { - if s > i128::MAX as u128 || k > i128::MAX as u128 || r > i128::MAX as u128 - || sigma > i128::MAX as u128 || t > i128::MAX as u128 + if s > i128::MAX as u128 + || k > i128::MAX as u128 + || r > i128::MAX as u128 + || sigma > i128::MAX as u128 + || t > i128::MAX as u128 { return Err(SolMathError::Overflow); } @@ -680,9 +730,10 @@ pub fn black_scholes_price_hp( // fp_mul_hp_i terms are checked; s_hp and k_disc_hp are SCALE_HP prices, phi values ∈ [0, SCALE_HP]; // each product ≤ SCALE_HP; differences ∈ (-SCALE_HP, SCALE_HP), fits i128. let call_hp = fp_mul_hp_i(im.s_hp, im.phi_d1_hp)? - fp_mul_hp_i(im.k_disc_hp, im.phi_d2_hp)?; - let put_hp = fp_mul_hp_i(im.k_disc_hp, im.phi_neg_d2_hp)? - fp_mul_hp_i(im.s_hp, im.phi_neg_d1_hp)?; - let call = downscale_hp_to_std(call_hp); - let put = downscale_hp_to_std(put_hp); + let call_std = downscale_hp_to_std(call_hp); + let k_disc_std = downscale_hp_to_std_i(im.k_disc_hp); + let (call, put) = + crate::arithmetic::european_prices_from_call(call_std as i128, s, k_disc_std)?; Ok((call, put)) } @@ -701,6 +752,7 @@ pub fn black_scholes_price_hp( /// - **Errors**: `DomainError` if `sigma == 0` or `t == 0`. /// - **Accuracy**: call/put 3-4 ULP max, ~74% exact. Gamma 1 ULP max, 100% exact. /// Delta 1 ULP max, 99.9% exact. +/// - **CU**: final SBF audit 113,177 average, 112,816 median, 149,925 max. /// /// # Example /// ``` @@ -712,8 +764,11 @@ pub fn black_scholes_price_hp( /// # Ok::<(), solmath::SolMathError>(()) /// ``` pub fn bs_full_hp(s: u128, k: u128, r: u128, sigma: u128, t: u128) -> Result { - if s > i128::MAX as u128 || k > i128::MAX as u128 || r > i128::MAX as u128 - || sigma > i128::MAX as u128 || t > i128::MAX as u128 + if s > i128::MAX as u128 + || k > i128::MAX as u128 + || r > i128::MAX as u128 + || sigma > i128::MAX as u128 + || t > i128::MAX as u128 { return Err(SolMathError::Overflow); } @@ -722,16 +777,34 @@ pub fn bs_full_hp(s: u128, k: u128, r: u128, sigma: u128, t: u128) -> Result 0 { s } else { 0 }, put: if s == 0 { let r_t = fp_mul_i(r as i128, t as i128)?; let kd = fp_mul_i(k as i128, exp_fixed_i(-r_t)?)?; - if kd > 0 { kd as u128 } else { 0 } - } else { 0 }, + if kd > 0 { + kd as u128 + } else { + 0 + } + } else { + 0 + }, call_delta: if s == 0 { 0 } else { SCALE_I }, put_delta: if s == 0 { -SCALE_I } else { 0 }, - gamma: 0, vega: 0, call_theta: 0, put_theta: 0, call_rho: 0, put_rho: 0, + gamma: 0, + vega: 0, + call_theta: 0, + put_theta, + call_rho: 0, + put_rho, }; return Ok(zero_full); } @@ -746,9 +819,10 @@ pub fn bs_full_hp(s: u128, k: u128, r: u128, sigma: u128, t: u128) -> Result Result 0 { -fp_div_hp_safe(spd_sigma_hp, two_sqrt_t_hp)? } else { @@ -831,7 +905,10 @@ pub(crate) fn horner_compensated_hp(coeffs: &[i128], t: i128) -> Result Result { +pub(crate) fn horner_compensated_hp_dw( + coeffs: &[i128], + t: i128, +) -> Result { let n = coeffs.len(); if n <= 1 { return Ok(DoubleWord::from_hi(if n == 1 { coeffs[0] } else { 0 })); @@ -846,8 +923,7 @@ pub(crate) fn horner_compensated_hp_dw(coeffs: &[i128], t: i128) -> Result Result i64 { #[inline(always)] fn div_h(a: i64, b: i64) -> i64 { debug_assert!(b != 0, "div_h: divisor must be non-zero"); - if b == 0 { return if a >= 0 { i64::MAX } else { i64::MIN }; } + if b == 0 { + return if a >= 0 { i64::MAX } else { i64::MIN }; + } (((a as i128) << SHIFT) / b as i128) as i64 } @@ -53,13 +55,23 @@ fn div_h(a: i64, b: i64) -> i64 { /// exp at SCALE_H. Range reduction via shift, Taylor remainder. fn exp_h(x: i64) -> i64 { - if x >= 15 * SH { return i64::MAX / 4; } - if x <= -15 * SH { return 0; } - if x == 0 { return SH; } + if x >= 15 * SH { + return i64::MAX / 4; + } + if x <= -15 * SH { + return 0; + } + if x == 0 { + return SH; + } // Range reduce: x = k·ln2 + r, |r| ≤ ln2/2 let half_ln2 = LN2_H / 2; - let k = if x >= 0 { (x + half_ln2) / LN2_H } else { (x - half_ln2) / LN2_H }; + let k = if x >= 0 { + (x + half_ln2) / LN2_H + } else { + (x - half_ln2) / LN2_H + }; let r = x - k * LN2_H; // Taylor degree 10: exp(r) = Σ rⁿ/n! (converges fast for |r| < 0.347×SH) @@ -69,21 +81,35 @@ fn exp_h(x: i64) -> i64 { while n <= 10 { term = mul_h(term, r) / n; sum += term; - if term == 0 { break; } + if term == 0 { + break; + } n += 1; } - if k >= 0 { sum << (k as u32) } else { sum >> ((-k) as u32) } + if k >= 0 { + sum << (k as u32) + } else { + sum >> ((-k) as u32) + } } /// ln at SCALE_H. Arctanh series: ln(m) = 2·t·(1 + t²/3 + t⁴/5 + ...) /// where t = (m − SH)/(m + SH). fn ln_h(x: i64) -> i64 { - if x <= 0 { return i64::MIN; } + if x <= 0 { + return i64::MIN; + } let mut m = x; let mut k: i32 = 0; - while m < SH { m *= 2; k -= 1; } - while m >= 2 * SH { m /= 2; k += 1; } + while m < SH { + m *= 2; + k -= 1; + } + while m >= 2 * SH { + m /= 2; + k += 1; + } let t = div_h(m - SH, m + SH); let t2 = mul_h(t, t); @@ -95,7 +121,9 @@ fn ln_h(x: i64) -> i64 { sum += pw / d; pw = mul_h(pw, t2); d += 2; - if pw.abs() < 1 { break; } + if pw.abs() < 1 { + break; + } i += 1; } 2 * sum + (k as i64) * LN2_H @@ -103,15 +131,21 @@ fn ln_h(x: i64) -> i64 { /// sqrt at SCALE_H. Newton iteration on scaled value. fn sqrt_h(x: i64) -> i64 { - if x <= 0 { return 0; } + if x <= 0 { + return 0; + } let scaled = (x as i128) << SHIFT; let bl = 128 - (scaled as u128).leading_zeros(); let mut g: i128 = 1i128 << ((bl + 1) / 2).min(62); let mut i = 0; while i < 6 { - if g == 0 { break; } + if g == 0 { + break; + } let ng = (g + scaled / g) / 2; - if ng >= g { break; } + if ng >= g { + break; + } g = ng; i += 1; } @@ -123,8 +157,12 @@ fn sqrt_h(x: i64) -> i64 { fn mod_2pi_h(x: i64) -> i64 { let pi2 = 2 * PI_H; let mut r = x % pi2; - if r > PI_H { r -= pi2; } - if r < -PI_H { r += pi2; } + if r > PI_H { + r -= pi2; + } + if r < -PI_H { + r += pi2; + } r } @@ -132,10 +170,10 @@ fn mod_2pi_h(x: i64) -> i64 { fn sin_core_h(x: i64) -> i64 { let x2 = mul_h(x, x); // sin(x)/x ≈ 1 − x²/6 + x⁴/120 − x⁶/5040 - let mut r = -SH / 5040; // c6 (tiny) - r = mul_h(r, x2) + SH / 120; // c4 - r = mul_h(r, x2) - SH / 6; // c2 - r = mul_h(r, x2) + SH; // c0 + let mut r = -SH / 5040; // c6 (tiny) + r = mul_h(r, x2) + SH / 120; // c4 + r = mul_h(r, x2) - SH / 6; // c2 + r = mul_h(r, x2) + SH; // c0 mul_h(r, x) } @@ -149,11 +187,41 @@ fn cos_core_h(x: i64) -> i64 { r } +/// Cosine only at SCALE_H — same reduction as sincos_h without evaluating +/// the sine polynomial. Bit-identical to sincos_h's cosine output. +fn cos_h(x: i64) -> i64 { + let mut xx = mod_2pi_h(x); + if xx < 0 { + xx = -xx; // cos is even + } + let cos_sign: i64 = if xx > PIH_H { + xx = PI_H - xx; + -1 + } else { + 1 + }; + if xx > PIQ_H { + sin_core_h(PIH_H - xx) * cos_sign + } else { + cos_core_h(xx) * cos_sign + } +} + /// Fused sin+cos at SCALE_H. fn sincos_h(x: i64) -> (i64, i64) { let mut xx = mod_2pi_h(x); - let sin_sign: i64 = if xx < 0 { xx = -xx; -1 } else { 1 }; - let cos_sign: i64 = if xx > PIH_H { xx = PI_H - xx; -1 } else { 1 }; + let sin_sign: i64 = if xx < 0 { + xx = -xx; + -1 + } else { + 1 + }; + let cos_sign: i64 = if xx > PIH_H { + xx = PI_H - xx; + -1 + } else { + 1 + }; if xx > PIQ_H { let y = PIH_H - xx; (cos_core_h(y) * sin_sign, sin_core_h(y) * cos_sign) @@ -165,7 +233,9 @@ fn sincos_h(x: i64) -> (i64, i64) { /// Complex sqrt at SCALE_H. fn complex_sqrt_h(re: i64, im: i64) -> (i64, i64) { let nsq = mul_h(re, re) + mul_h(im, im); - if nsq == 0 { return (0, 0); } + if nsq == 0 { + return (0, 0); + } let modz = sqrt_h(nsq); let re_arg = (modz + re) / 2; let out_re = if re_arg > 0 { sqrt_h(re_arg) } else { 0 }; @@ -189,12 +259,13 @@ fn atan_poly_h(t: i64) -> i64 { } fn atan_01_h(z: i64) -> i64 { - const TAN15: i64 = 280_870; // tan(π/12) × 2^20 - const TAN30: i64 = 605_382; // tan(π/6) × 2^20 - const PI_6C: i64 = 549_033; // π/6 × 2^20 + const TAN15: i64 = 280_870; // tan(π/12) × 2^20 + const TAN30: i64 = 605_382; // tan(π/6) × 2^20 + const PI_6C: i64 = 549_033; // π/6 × 2^20 if z <= TAN15 { atan_poly_h(z) - } else if z <= 786_432 { // ~0.75 × SH + } else if z <= 786_432 { + // ~0.75 × SH let num = z - TAN30; let den = SH + mul_h(z, TAN30); PI_6C + atan_poly_h(div_h(num, den)) @@ -204,9 +275,15 @@ fn atan_01_h(z: i64) -> i64 { } pub(crate) fn atan2_h(y: i64, x: i64) -> i64 { - if x == 0 && y == 0 { return 0; } - if x == 0 { return if y > 0 { PIH_H } else { -PIH_H }; } - if y == 0 { return if x > 0 { 0 } else { PI_H }; } + if x == 0 && y == 0 { + return 0; + } + if x == 0 { + return if y > 0 { PIH_H } else { -PIH_H }; + } + if y == 0 { + return if x > 0 { 0 } else { PI_H }; + } let ax = x.unsigned_abs() as i64; let ay = y.unsigned_abs() as i64; let swap = ay > ax; @@ -215,7 +292,11 @@ pub(crate) fn atan2_h(y: i64, x: i64) -> i64 { let a = atan_01_h(z); let a = if swap { PIH_H - a } else { a }; let a = if x < 0 { PI_H - a } else { a }; - if y < 0 { -a } else { a } + if y < 0 { + -a + } else { + a + } } // ============================================================ @@ -227,10 +308,15 @@ pub(crate) fn atan2_h(y: i64, x: i64) -> i64 { /// /// `mu` = κθ/ξ² is loop-invariant — precomputed by caller. pub(crate) fn heston_cv_node_h( - u: i64, x: i64, t: i64, v0: i64, - m_re: i64, m_im_coeff: i64, // m_re = κ − ρξ/2, m_im_coeff = −ρξ - xi_sq: i64, xi_sq_1mrho: i64, // ξ², ξ²(1−ρ²) - mu: i64, // κθ/ξ² (loop-invariant) + u: i64, + x: i64, + t: i64, + v0: i64, + m_re: i64, + m_im_coeff: i64, // m_re = κ − ρξ/2, m_im_coeff = −ρξ + xi_sq: i64, + xi_sq_1mrho: i64, // ξ², ξ²(1−ρ²) + mu: i64, // κθ/ξ² (loop-invariant) seff_sq_t_half: i64, ) -> i64 { let u_sq = mul_h(u, u); @@ -238,9 +324,7 @@ pub(crate) fn heston_cv_node_h( let m_im = mul_h(m_im_coeff, u); - let d2_re = mul_h(m_re, m_re) - + mul_h(xi_sq_1mrho, u_sq) - + xi_sq / 4; + let d2_re = mul_h(m_re, m_re) + mul_h(xi_sq_1mrho, u_sq) + xi_sq / 4; let d2_im = 2 * mul_h(m_re, m_im); let (d_re, d_im) = complex_sqrt_h(d2_re, d2_im); @@ -266,11 +350,15 @@ pub(crate) fn heston_cv_node_h( // inv_p = SH / p_mod2 (one true division, then 4 shifts) let (d_coeff_re, d_coeff_im, ratio_re, ratio_im) = if p_mod2 != 0 { let inv_p = div_h(SH, p_mod2); // single division - (mul_h(mul_h(dn_re, p_re) + mul_h(dn_im, p_im), inv_p), - mul_h(mul_h(dn_im, p_re) - mul_h(dn_re, p_im), inv_p), - mul_h(mul_h(2*d_re, p_re) + mul_h(2*d_im, p_im), inv_p), - mul_h(mul_h(2*d_im, p_re) - mul_h(2*d_re, p_im), inv_p)) - } else { (0, 0, SH, 0) }; + ( + mul_h(mul_h(dn_re, p_re) + mul_h(dn_im, p_im), inv_p), + mul_h(mul_h(dn_im, p_re) - mul_h(dn_re, p_im), inv_p), + mul_h(mul_h(2 * d_re, p_re) + mul_h(2 * d_im, p_im), inv_p), + mul_h(mul_h(2 * d_im, p_re) - mul_h(2 * d_re, p_im), inv_p), + ) + } else { + (0, 0, SH, 0) + }; let ratio_mod = sqrt_h(mul_h(ratio_re, ratio_re) + mul_h(ratio_im, ratio_im)); let ln_ratio_mod = if ratio_mod > 0 { ln_h(ratio_mod) } else { 0 }; @@ -278,21 +366,16 @@ pub(crate) fn heston_cv_node_h( let two_mu = 2 * mu; - let real_exp = mul_h(d_coeff_re, v0) - + mul_h(mu, mul_h(mm_re, t)) - + mul_h(two_mu, ln_ratio_mod); - let imag_exp = mul_h(d_coeff_im, v0) - + mul_h(mu, mul_h(mm_im, t)) - + mul_h(two_mu, arg_ratio); + let real_exp = mul_h(d_coeff_re, v0) + mul_h(mu, mul_h(mm_re, t)) + mul_h(two_mu, ln_ratio_mod); + let imag_exp = mul_h(d_coeff_im, v0) + mul_h(mu, mul_h(mm_im, t)) + mul_h(two_mu, arg_ratio); - let total_angle = imag_exp + mul_h(u, x); + let ux = mul_h(u, x); + let total_angle = imag_exp + ux; let final_mag = exp_h(real_exp); - let (_, cos_hv) = sincos_h(total_angle); - let re_phi_h = mul_h(final_mag, cos_hv); + let re_phi_h = mul_h(final_mag, cos_h(total_angle)); let phi_bs = exp_h(-mul_h(seff_sq_t_half, uq)); - let (_, cos_bs) = sincos_h(mul_h(u, x)); - let re_phi_bs = mul_h(phi_bs, cos_bs); + let re_phi_bs = mul_h(phi_bs, cos_h(ux)); div_h(re_phi_bs - re_phi_h, uq) } @@ -322,15 +405,25 @@ pub(crate) fn to_h_i(v: i128) -> i64 { pub(crate) const PI_H_PUB: i64 = PI_H; #[inline(always)] -pub(crate) fn mul_h_pub(a: i64, b: i64) -> i64 { mul_h(a, b) } +pub(crate) fn mul_h_pub(a: i64, b: i64) -> i64 { + mul_h(a, b) +} #[inline(always)] -pub(crate) fn div_h_pub(a: i64, b: i64) -> i64 { div_h(a, b) } +pub(crate) fn div_h_pub(a: i64, b: i64) -> i64 { + div_h(a, b) +} #[inline(always)] -pub(crate) fn exp_h_pub(x: i64) -> i64 { exp_h(x) } +pub(crate) fn exp_h_pub(x: i64) -> i64 { + exp_h(x) +} #[inline(always)] -pub(crate) fn ln_h_pub(x: i64) -> i64 { ln_h(x) } +pub(crate) fn ln_h_pub(x: i64) -> i64 { + ln_h(x) +} #[inline(always)] -pub(crate) fn sqrt_h_pub(x: i64) -> i64 { sqrt_h(x) } +pub(crate) fn sqrt_h_pub(x: i64) -> i64 { + sqrt_h(x) +} // ============================================================ // Tests @@ -339,8 +432,8 @@ pub(crate) fn sqrt_h_pub(x: i64) -> i64 { sqrt_h(x) } #[cfg(test)] mod tests { use super::*; - use crate::transcendental::exp_fixed_i; use crate::arithmetic::fp_mul_i; + use crate::transcendental::exp_fixed_i; // Downscale from SCALE (1e12) to SCALE_H (2^20) fn to_h_test(v: i128) -> i64 { @@ -357,11 +450,18 @@ mod tests { let ref_val = exp_fixed_i(x_128).unwrap(); let test_val = exp_h(x_h); let ref_h = to_h_test(ref_val); - if ref_h == 0 { continue; } + if ref_h == 0 { + continue; + } let rel_err_ppm = ((ref_h - test_val).abs() as i128 * 1_000_000) / ref_h as i128; - assert!(rel_err_ppm < 2000, + assert!( + rel_err_ppm < 2000, "exp_h({}) = {}, expected {}, rel_err = {} ppm", - x_int, test_val, ref_h, rel_err_ppm); + x_int, + test_val, + ref_h, + rel_err_ppm + ); } } @@ -375,9 +475,14 @@ mod tests { let test_val = ln_h(mult); let ref_h = to_h_test(ref_val); let err = (ref_h - test_val).abs(); - assert!(err < 1000, + assert!( + err < 1000, "ln_h({}) = {}, expected {}, err = {}", - mult, test_val, ref_h, err); + mult, + test_val, + ref_h, + err + ); } } @@ -395,9 +500,13 @@ mod tests { let ref_ch = to_h_test(ref_c); let err_s = (ref_sh - test_s).abs(); let err_c = (ref_ch - test_c).abs(); - assert!(err_s < 1000 && err_c < 1000, + assert!( + err_s < 1000 && err_c < 1000, "sincos_h({:.1}) sin_err={} cos_err={}", - angle_x10 as f64 / 10.0, err_s, err_c); + angle_x10 as f64 / 10.0, + err_s, + err_c + ); } } @@ -406,10 +515,10 @@ mod tests { #[test] fn test_atan2_h_vs_i128() { let cases: [(i64, i64); 4] = [ - (SH, SH), // π/4 - (SH, -SH), // 3π/4 - (-SH, SH), // -π/4 - (SH / 10, SH), // ~0.0997 + (SH, SH), // π/4 + (SH, -SH), // 3π/4 + (-SH, SH), // -π/4 + (SH / 10, SH), // ~0.0997 ]; for (y, x) in cases { let y_128 = y as i128 * SCALE_TO_H; @@ -418,9 +527,15 @@ mod tests { let test_val = atan2_h(y, x); let ref_h = to_h_test(ref_val); let err = (ref_h - test_val).abs(); - assert!(err < 2000, + assert!( + err < 2000, "atan2_h({}, {}) = {}, expected {}, err = {}", - y, x, test_val, ref_h, err); + y, + x, + test_val, + ref_h, + err + ); } } @@ -453,17 +568,27 @@ mod tests { let xi_sq = mul_h(xih, xih); let rho_sq = mul_h(rhoh, rhoh); let xi_sq_1mrho = mul_h(xi_sq, SH - rho_sq); - let mu = if xi_sq != 0 { div_h(mul_h(kh, to_h(theta)), xi_sq) } else { 0 }; + let mu = if xi_sq != 0 { + div_h(mul_h(kh, to_h(theta)), xi_sq) + } else { + 0 + }; let u_h = 5 * SH; let result = heston_cv_node_h( - u_h, to_h_i(x_128), to_h(t), to_h(v0), - m_re, m_im_coeff, xi_sq, xi_sq_1mrho, - mu, to_h(crate::arithmetic::fp_mul(sigma_eff_sq, t).unwrap() / 2), + u_h, + to_h_i(x_128), + to_h(t), + to_h(v0), + m_re, + m_im_coeff, + xi_sq, + xi_sq_1mrho, + mu, + to_h(crate::arithmetic::fp_mul(sigma_eff_sq, t).unwrap() / 2), ); - assert!(result.abs() < SH, - "CV node result out of range: {}", result); + assert!(result.abs() < SH, "CV node result out of range: {}", result); } } diff --git a/src/i64_math.rs b/src/i64_math.rs index 6fc34cc..1cd96b5 100644 --- a/src/i64_math.rs +++ b/src/i64_math.rs @@ -1,274 +1,42 @@ -use crate::constants::*; +//! Compatibility NIG interface at 1e6 fixed-point scale. +//! +//! These entry points retain the published signatures, convert into the +//! checked 1e12 production implementation, and use zero dividend yield. New +//! integrations should use [`crate::nig_price_certified`] directly so the +//! dividend yield and requested absolute error are explicit. + use crate::error::SolMathError; +use crate::nig::{nig_price_certified, NigParams}; -#[derive(Clone, Copy)] -pub(crate) struct Complex6 { - pub re: i64, - pub im: i64, -} +const SCALE_BRIDGE: i128 = 1_000_000; -impl Complex6 { - /// Construct a complex number at 1e6 scale. Internal. - pub(crate) fn new(re: i64, im: i64) -> Self { - Self { re, im } - } -} - -/// Fixed-point multiply at 1e6 scale. Internal. -/// Returns `Err(Overflow)` if the result exceeds `i64` range. #[inline] -pub(crate) fn mul6(a: i64, b: i64) -> Result { - let wide = (a as i128 * b as i128) / SCALE_6 as i128; - if wide > i64::MAX as i128 || wide < i64::MIN as i128 { - return Err(SolMathError::Overflow); - } - Ok(wide as i64) +fn to_scale12(value: i64) -> Result { + (value as i128) + .checked_mul(SCALE_BRIDGE) + .ok_or(SolMathError::Overflow) } -/// Fixed-point divide at 1e6 scale. Internal. -/// Returns `Err(DivisionByZero)` if `b == 0`, `Err(Overflow)` if result exceeds `i64` range. #[inline] -pub(crate) fn div6(a: i64, b: i64) -> Result { - if b == 0 { - return Err(SolMathError::DivisionByZero); - } - let wide = (a as i128 * SCALE_6 as i128) / b as i128; - if wide > i64::MAX as i128 || wide < i64::MIN as i128 { - return Err(SolMathError::Overflow); - } - Ok(wide as i64) -} - -/// Natural logarithm at 1e6 scale. Internal. -/// Returns `Err(DomainError)` if `x <= 0`. -pub(crate) fn ln6(x: i64) -> Result { - if x <= 0 { - return Err(SolMathError::DomainError); - } - let mut m = x; - let mut k: i32 = 0; - while m < SCALE_6 { - m *= 2; - k -= 1; - } - while m >= 2 * SCALE_6 { - m /= 2; - k += 1; - } - let t = div6(m - SCALE_6, m + SCALE_6)?; - let t2 = mul6(t, t)?; - let mut sum = 0i64; - let mut pw = t; - let mut d = 1i64; - for _ in 0..10 { - sum += pw / d; - pw = mul6(pw, t2)?; - d += 2; - if pw.unsigned_abs() < 1 { - break; - } - } - Ok(2 * sum + (k as i64) * LN2_6) -} - -/// Exponential at 1e6 scale. Internal. -/// Returns `Err(Overflow)` if `x >= 20 * SCALE_6`. Returns `Ok(0)` for large negative x. -pub(crate) fn exp6(x: i64) -> Result { - let max_x = 20 * SCALE_6; - if x >= max_x { - return Err(SolMathError::Overflow); - } - if x <= -max_x { - return Ok(0); - } - if x == 0 { - return Ok(SCALE_6); - } - - let mut k = x / LN2_6; - let mut r = x - k * LN2_6; - let half = LN2_6 / 2; - if r > half { - k += 1; - r -= LN2_6; - } else if r < -half { - k -= 1; - r += LN2_6; - } - - let mut term = SCALE_6; - let mut sum = SCALE_6; - for n in 1..=10i64 { - term = mul6(term, r)? / n; - sum += term; - if term == 0 { - break; - } - } - - if k >= 0 { - let result = (sum as i128).checked_shl(k as u32).ok_or(SolMathError::Overflow)?; - if result > i64::MAX as i128 { - return Err(SolMathError::Overflow); - } - Ok(result as i64) - } else { - Ok(sum >> ((-k) as u32)) - } -} - -/// Square root at 1e6 scale. Internal. -/// Returns `Err(DomainError)` if `x < 0`, `Ok(0)` if `x == 0`. -pub(crate) fn sqrt6(x: i64) -> Result { - if x < 0 { +fn to_scale12_unsigned(value: i64) -> Result { + if value < 0 { return Err(SolMathError::DomainError); } - if x == 0 { - return Ok(0); - } - let scaled = x as i128 * SCALE_6 as i128; - let bl = 128 - scaled.leading_zeros(); - let mut g: i128 = 1i128 << ((bl + 1) / 2).min(62); - for _ in 0..6 { - if g == 0 { - break; - } - let ng = (g + scaled / g) / 2; - if ng >= g { - break; - } - g = ng; - } - Ok(g as i64) + Ok(to_scale12(value)? as u128) } -/// Reduce angle to (−π, π] at 1e6 scale. Internal. #[inline] -pub(crate) fn mod_2pi_6(x: i64) -> i64 { - const PI2_12: i128 = 6_283_185_307_180; - const UP: i128 = 1_000_000; - let x_hi = x as i128 * UP; - let pi_12 = PI2_12 / 2; - let mut r = x_hi % PI2_12; - if r > pi_12 { - r -= PI2_12; - } - if r < -pi_12 { - r += PI2_12; - } - (r / UP) as i64 +fn from_scale12(value: u128) -> Result { + let rounded = value + .checked_add((SCALE_BRIDGE as u128) / 2) + .ok_or(SolMathError::Overflow)? + / SCALE_BRIDGE as u128; + i64::try_from(rounded).map_err(|_| SolMathError::Overflow) } -/// Core sin on [−π/4, π/4] at 1e6 scale. Internal. -pub(crate) fn sin_core6(x: i64) -> Result { - let t = mul6(x, x)?; - let mut r = SC4_6; - r = mul6(r, t)? + SC3_6; - r = mul6(r, t)? + SC2_6; - r = mul6(r, t)? + SC1_6; - r = mul6(r, t)? + SC0_6; - mul6(r, x) -} - -/// Core cos on [−π/4, π/4] at 1e6 scale. Internal. -pub(crate) fn cos_core6(x: i64) -> Result { - let t = mul6(x, x)?; - let mut r = CC4_6; - r = mul6(r, t)? + CC3_6; - r = mul6(r, t)? + CC2_6; - r = mul6(r, t)? + CC1_6; - r = mul6(r, t)? + CC0_6; - Ok(r) -} - -/// Fused sin+cos at 1e6 scale. Internal. -pub(crate) fn sincos6(x: i64) -> Result<(i64, i64), SolMathError> { - let mut xx = mod_2pi_6(x); - let sin_sign = if xx < 0 { - xx = -xx; - -1i64 - } else { - 1 - }; - let cos_sign = if xx > PIH_6 { - xx = PI6 - xx; - -1i64 - } else { - 1 - }; - if xx > PIQ_6 { - let y = PIH_6 - xx; - Ok((cos_core6(y)? * sin_sign, sin_core6(y)? * cos_sign)) - } else { - Ok((sin_core6(xx)? * sin_sign, cos_core6(xx)? * cos_sign)) - } -} - -/// Complex multiply at 1e6 scale. Internal. -/// Uses i128 intermediates for subtraction/addition to prevent i64 overflow. -pub(crate) fn complex_mul6(a: Complex6, b: Complex6) -> Result { - let re_wide = mul6(a.re, b.re)? as i128 - mul6(a.im, b.im)? as i128; - let im_wide = mul6(a.re, b.im)? as i128 + mul6(a.im, b.re)? as i128; - if re_wide > i64::MAX as i128 || re_wide < i64::MIN as i128 - || im_wide > i64::MAX as i128 || im_wide < i64::MIN as i128 - { - return Err(SolMathError::Overflow); - } - Ok(Complex6::new(re_wide as i64, im_wide as i64)) -} - -/// Complex exponential at 1e6 scale. Internal. -pub(crate) fn complex_exp6(z: Complex6) -> Result { - let e = exp6(z.re)?; - let (s, c) = sincos6(z.im)?; - Ok(Complex6::new(mul6(e, c)?, mul6(e, s)?)) -} - -/// Complex square root at 1e6 scale. Internal. -pub(crate) fn complex_sqrt6(z: Complex6) -> Result { - let nsq = mul6(z.re, z.re)? + mul6(z.im, z.im)?; - if nsq == 0 { - return Ok(Complex6::new(0, 0)); - } - let modz = sqrt6(nsq)?; - let re_arg = (modz + z.re) / 2; - let re = if re_arg > 0 { sqrt6(re_arg)? } else { 0 }; - if re == 0 { - let im = sqrt6((modz - z.re) / 2)?; - return Ok(Complex6::new(0, if z.im < 0 { -im } else { im })); - } - let im = div6(z.im, 2 * re)?; - Ok(Complex6::new(re, im)) -} - -const NIG_N_6: usize = 17; -const NIG_L_6: i64 = 6_750_000; // 6.75 * SCALE_6 - -/// NIG characteristic function at i64 scale. -pub(crate) fn nig_char6(u: i64, drift: i64, dt: i64, gamma: i64, asq: i64, beta: i64) -> Result { - let usq = mul6(u, u)?; - let bsq = mul6(beta, beta)?; - let inner = complex_sqrt6(Complex6::new(asq - bsq + usq, -2 * mul6(beta, u)?))?; - let exp_arg = Complex6::new( - mul6(dt, gamma - inner.re)?, - mul6(u, drift)? - mul6(dt, inner.im)?, - ); - complex_exp6(exp_arg) -} - -/// On-chain NIG call pricing via COS method (17 terms, i64 arithmetic). -/// ~120K CU. Inputs at SCALE (1e12), computed internally at 1e6. -/// -/// # Errors -/// - `DomainError` if parameters are invalid (α² ≤ β², γ < 5, |β/α| ≥ 0.9, etc.) -/// - `Overflow` if intermediate arithmetic overflows. -/// -/// # Precision -/// 95% within 0.5% for α ≥ 10, prices > $1. -/// -/// # CU cost -/// ~120,000 CU. -pub fn nig_call_64( +#[allow(clippy::too_many_arguments)] +fn price_64( + call: bool, s: i64, k: i64, r: i64, @@ -277,172 +45,51 @@ pub fn nig_call_64( beta: i64, delta: i64, ) -> Result { - if s <= 0 || k <= 0 || t <= 0 || alpha <= 0 || delta <= 0 { - return Err(SolMathError::DomainError); - } - // Domain: alpha ≤ 10,000. Real NIG calibrations have alpha in [1, 100]. - if alpha > 10_000 * SCALE_6 { + if s <= 0 || k <= 0 || t < 0 || alpha <= 0 || delta <= 0 { return Err(SolMathError::DomainError); } - let asq = mul6(alpha, alpha)?; - let bsq = mul6(beta, beta)?; - if asq <= bsq { - return Err(SolMathError::DomainError); - } - let gamma = sqrt6(asq - bsq)?; - if gamma < 5 * SCALE_6 { - return Err(SolMathError::DomainError); - } - if beta == i64::MIN { - return Err(SolMathError::DomainError); - } - if beta.abs() * 10 >= alpha * 9 { - return Err(SolMathError::DomainError); - } - if gamma <= 0 { - return Err(SolMathError::DomainError); - } - let gcu = mul6(mul6(gamma, gamma)?, gamma)?; - if gcu == 0 { - return Err(SolMathError::Overflow); - } - - let bp1 = beta + SCALE_6; - let omega = mul6(delta, gamma - sqrt6(asq - mul6(bp1, bp1)?)?)?; - - let ln_s = ln6(s)?; - let ln_k = ln6(k)?; - let dr = r - omega; - let c1 = ln_s + mul6(dr, t)? + div6(mul6(mul6(delta, t)?, beta)?, gamma)?; - let c2 = div6(mul6(mul6(delta, t)?, asq)?, gcu)?; - let std = sqrt6(c2)?; - - let l_std = mul6(NIG_L_6, std)?; - let mut a = c1 - l_std; - let mut b = c1 + l_std; - if ln_k - std < a { - a = ln_k - std; - } - if ln_k + std > b { - b = ln_k + std; - } - let ba = b - a; - if ba <= 0 { - return Err(SolMathError::DomainError); - } - - let disc = exp6(-mul6(r, t)?)?; - let exp_b = exp6(b)?; - let is_otm = ln_k > c1; - let exp_a = if is_otm { exp6(a)? } else { 0 }; - - let cf_drift = ln_s + mul6(dr, t)?; - let dt = mul6(delta, t)?; - let gsq = asq - bsq; - - let lk_a = ln_k - a; - let theta_v = div6(mul6(PI6, lk_a)?, ba)?; - let (sin_tv, cos_tv) = sincos6(theta_v)?; - let theta_r = div6(mul6(PI6, a)?, ba)?; - let (sin_tr, cos_tr) = sincos6(theta_r)?; - - let (mut vc0, mut vs0) = (SCALE_6, 0i64); - let (mut vc1, mut vs1) = (cos_tv, sin_tv); - let (mut rc0, mut rs0) = (SCALE_6, 0i64); - let (mut rc1, mut rs1) = (cos_tr, sin_tr); - - let vk0 = if is_otm { - let chi = k - exp_a; - let psi = ln_k - a; - div6(2 * (mul6(k, psi)? - chi), ba)? - } else { - let chi = exp_b - k; - let psi = b - ln_k; - div6(2 * (chi - mul6(k, psi)?), ba)? - }; - let mut total: i64 = mul6(SCALE_6 / 2, vk0)?; - - let mut i = 1usize; - while i < NIG_N_6 { - let w = div6((i as i64) * PI6, ba)?; - - if mul6(dt, gamma - w)? < -3 * SCALE_6 { - break; - } - - let wsq = mul6(w, w)?; - let phi = if wsq > 4 * gsq { - let inner_re = w + div6(gsq, 2 * w)?; - let z_im = -2 * mul6(beta, w)?; - let inner_im = div6(z_im, 2 * inner_re)?; - let exp_arg = Complex6::new( - mul6(dt, gamma - inner_re)?, - mul6(w, cf_drift)? - mul6(dt, inner_im)?, - ); - complex_exp6(exp_arg)? - } else { - nig_char6(w, cf_drift, dt, gamma, asq, beta)? - }; - let rot = Complex6::new(rc1, -rs1); - let ct = complex_mul6(phi, rot)?.re; - - let cost = vc1; - let sint = vs1; - let vk = if is_otm { - let chi = div6(mul6(k, cost + mul6(w, sint)?)? - exp_a, SCALE_6 + wsq)?; - let psi = div6(sint, w)?; - div6(2 * (mul6(k, psi)? - chi), ba)? + if t == 0 { + return Ok(if call { + s.saturating_sub(k) } else { - let sk: i64 = if i % 2 == 0 { 1 } else { -1 }; - let chi = div6(sk * exp_b - mul6(k, cost + mul6(w, sint)?)?, SCALE_6 + wsq)?; - let psi = -div6(sint, w)?; - div6(2 * (chi - mul6(k, psi)?), ba)? - }; - - total += mul6(ct, vk)?; - - let vc_next = (2 * mul6(cos_tv, vc1)? - vc0).clamp(-SCALE_6, SCALE_6); - let vs_next = (2 * mul6(cos_tv, vs1)? - vs0).clamp(-SCALE_6, SCALE_6); - vc0 = vc1; - vs0 = vs1; - vc1 = vc_next; - vs1 = vs_next; - - let rc_next = (2 * mul6(cos_tr, rc1)? - rc0).clamp(-SCALE_6, SCALE_6); - let rs_next = (2 * mul6(cos_tr, rs1)? - rs0).clamp(-SCALE_6, SCALE_6); - rc0 = rc1; - rs0 = rs1; - rc1 = rc_next; - rs1 = rs_next; - - i += 1; - } - - if is_otm { - let put = mul6(disc, total)?; - let put = if put > 0 { put } else { 0 }; - let call = put + s - mul6(k, disc)?; - Ok(if call > 0 { call } else { 0 }) - } else { - let call = mul6(disc, total)?; - Ok(if call > 0 { call } else { 0 }) - } + k.saturating_sub(s) + }); + } + + let spot = to_scale12_unsigned(s)?; + let strike = to_scale12_unsigned(k)?; + // Legacy compatibility target: 5e-5 of notional, i.e. $0.005 per $100. + let requested_max_abs_error = spot.max(strike).checked_div(20_000).unwrap_or(0).max(1); + let quote = nig_price_certified( + spot, + strike, + to_scale12(r)?, + 0, + to_scale12_unsigned(t)?, + NigParams { + alpha: to_scale12_unsigned(alpha)?, + beta: to_scale12(beta)?, + delta_per_year: to_scale12_unsigned(delta)?, + }, + requested_max_abs_error, + )?; + from_scale12(if call { quote.call } else { quote.put }) +} + +/// NIG call API at 1e6 scale (`q = 0`, default `$0.005 / $100` request). +pub fn nig_call_64( + s: i64, + k: i64, + r: i64, + t: i64, + alpha: i64, + beta: i64, + delta: i64, +) -> Result { + price_64(true, s, k, r, t, alpha, beta, delta) } -/// On-chain NIG put pricing via put-call parity. ~120K CU. -/// Inputs at SCALE (1e12), computed internally at 1e6. -/// -/// Put = Call - S + K × e^(-rT), computed using i64 helpers internally. -/// -/// # Errors -/// - `DomainError` if parameters are invalid (same as `nig_call_64`). -/// - `Overflow` if intermediate arithmetic overflows. -/// -/// # Precision -/// Same as `nig_call_64` — 95% within 0.5% for α ≥ 10, prices > $1. -/// -/// # CU cost -/// ~120,000 CU. +/// NIG put API at 1e6 scale (`q = 0`, default `$0.005 / $100` request). pub fn nig_put_64( s: i64, k: i64, @@ -452,8 +99,45 @@ pub fn nig_put_64( beta: i64, delta: i64, ) -> Result { - let call = nig_call_64(s, k, r, t, alpha, beta, delta)?; - let disc = exp6(-mul6(r, t)?)?; - let put_i = call - s + mul6(k, disc)?; - Ok(if put_i > 0 { put_i } else { 0 }) + price_64(false, s, k, r, t, alpha, beta, delta) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn expiry_is_intrinsic() { + assert_eq!(nig_call_64(120, 100, 0, 0, 1, 0, 1), Ok(20)); + assert_eq!(nig_put_64(80, 100, 0, 0, 1, 0, 1), Ok(20)); + } + + #[test] + fn positive_expiry_routes_to_production_pricer() { + let call = nig_call_64( + 100_000_000, + 100_000_000, + 50_000, + 1_000_000, + 10_000_000, + -2_000_000, + 200_000, + ) + .unwrap(); + let put = nig_put_64( + 100_000_000, + 100_000_000, + 50_000, + 1_000_000, + 10_000_000, + -2_000_000, + 200_000, + ) + .unwrap(); + assert!(call > 0); + assert!(put > 0); + // Put-call parity at the legacy output precision. + let discounted_strike = 95_122_942; + assert!((call - put - (100_000_000 - discounted_strike)).abs() <= 2); + } } diff --git a/src/iv.rs b/src/iv.rs index 446e2a0..c40a69b 100644 --- a/src/iv.rs +++ b/src/iv.rs @@ -1,16 +1,31 @@ +use crate::arithmetic::{fp_div, fp_div_i, fp_mul, fp_mul_i, fp_mul_i_fast, fp_sqrt}; +use crate::bs::black_scholes_price; use crate::constants::*; use crate::error::SolMathError; -use crate::arithmetic::{fp_mul, fp_mul_i, fp_mul_i_fast, fp_div, fp_div_i, fp_sqrt}; -use crate::transcendental::{ln_fixed_i, exp_fixed_i}; -use crate::normal::{norm_cdf_poly, norm_pdf, norm_cdf_and_pdf, inverse_norm_cdf}; -use crate::bs::black_scholes_price; +use crate::normal::{inverse_norm_cdf, norm_cdf_and_pdf, norm_cdf_poly, norm_pdf}; +use crate::transcendental::{exp_fixed_i, ln_fixed_i}; /// Unchecked signed fixed-point multiply. Caller guarantees no overflow. /// Used only in IV solver where inputs are pre-validated and bounded. +/// +/// Precondition: `|a * b| < i128::MAX`. Every call site derives its operands +/// from one of three provably bounded sources — input log-moneyness (guarded by +/// the `implied_vol` domain checks), compile-time polynomial coefficients, or +/// `norm_cdf_poly` outputs clamped to `[0, SCALE_I]`. The only place a runaway +/// solver bracket could grow an operand without bound is `normalised_vega`, +/// which squares the total-vol state `s`; that site is guarded independently. +/// +/// The `debug_assert` below turns the precondition from an *assumption* into an +/// *enforced* invariant: it costs nothing in release builds, but any violation +/// aborts every `cargo test` / debug-fuzz run with the exact offending operands. +/// CI additionally runs the pricing fuzz under `overflow-checks=on` in release, +/// so a breach fails closed there too. #[inline(always)] fn mul_fast(a: i128, b: i128) -> i128 { - // a, b are SCALE-valued inputs bounded by domain guards; |a|,|b| < ~1e17 - // so |a * b| < 1e34 < i128::MAX (≈1.7e38). Division by SCALE_I restores scale. + debug_assert!( + a.checked_mul(b).is_some(), + "mul_fast precondition violated: {a} * {b} overflows i128 (IV solver bound broken)" + ); (a * b) / SCALE_I } @@ -102,7 +117,10 @@ pub(crate) fn li_rational_guess(x: i128, c: i128) -> Result } else { // Denominator non-positive — rare, fall back to ATM approximation // 2_506_628_274_631 ≈ √(2π)·SCALE_I; fp_div_i(c, fp_sqrt(c)) ≤ SCALE_I; mul_fast result ≤ ~2.51·SCALE_I. Fits i128. - Ok(mul_fast(2_506_628_274_631, fp_div_i(c, fp_sqrt(c as u128)? as i128)?)) // ≈ √(2π)·√c + Ok(mul_fast( + 2_506_628_274_631, + fp_div_i(c, fp_sqrt(c as u128)? as i128)?, + )) // ≈ √(2π)·√c } } @@ -111,6 +129,7 @@ pub(crate) fn li_rational_guess(x: i128, c: i128) -> Result /// σ√T ≈ β × P(z)/Q(z) where P,Q are degree-4 polynomials (9 coefficients). /// Replaces the 28-multiply Li bivariate form with fewer truncation errors. #[inline(never)] +#[cfg(feature = "pade-iv")] pub(crate) fn rational_guess_v2(x: i128, c: i128) -> Result { // β = c × √(2π) let beta = mul_fast(c, SQRT_2PI_IV); @@ -174,10 +193,18 @@ fn iv_price_and_greeks( // Subtraction of two such terms: |p|,|c| < ~2e17, fits i128. let price_i = if solve_as_put { let p = mul_fast(k_disc, SCALE_I - phi_d2) - mul_fast(s_i, SCALE_I - phi_d1); - if p > 0 { p } else { 0 } + if p > 0 { + p + } else { + 0 + } } else { let c = mul_fast(s_i, phi_d1) - mul_fast(k_disc, phi_d2); - if c > 0 { c } else { 0 } + if c > 0 { + c + } else { + 0 + } }; // s_i < ~1e17, pdf_d1 ∈ [0, SCALE_I/√(2π)] < SCALE_I; mul_fast result < ~1e17. @@ -203,34 +230,51 @@ fn halley_step_bracketed( x_lo: u128, x_hi: u128, ) -> Result { + let bisect = (x_lo + x_hi) / 2; if vega_x <= 1_000 { - return Ok((x_lo + x_hi) / 2); + return Ok(bisect); } - // f = price - target: both < ~1e17; vega_x < ~1e17. mul_fast < ~1e17. Factor 2: < ~2e17 < i128::MAX. - let two_f_fp = 2 * mul_fast(f, vega_x); - // mul_fast(vega_x, vega_x) < ~1e17; factor 2 < ~2e17. mul_fast(f, volga_x): both < ~1e17. Difference fits i128. - let denom = 2 * mul_fast(vega_x, vega_x) - mul_fast(f, volga_x); + // The Halley numerator/denominator combine price-scale `f` (~1e17) with the + // vega and volga sensitivities. Near ATM with tiny maturity `volga_x` blows + // up far past the price scale, so `f * volga_x` can exceed i128 even though + // every input is in range. Evaluate the products with checked arithmetic and + // fall back to the bisection midpoint (already this routine's safe step) on + // any overflow, rather than trusting an unbounded fast multiply. For in-range + // operands `fp_mul_i` equals the former `mul_fast` exactly, so accepted + // inputs are unaffected. + let halley_step = || -> Option { + let two_f_fp = fp_mul_i(f, vega_x).ok()?.checked_mul(2)?; + let two_vega_sq = fp_mul_i(vega_x, vega_x).ok()?.checked_mul(2)?; + let f_volga = fp_mul_i(f, volga_x).ok()?; + let denom = two_vega_sq.checked_sub(f_volga)?; + + let step = if denom.abs() > 1_000 { + fp_div_i(two_f_fp, denom).ok()? + } else { + fp_div_i(f, vega_x).ok()? + }; - let step = if denom.abs() > 1_000 { - fp_div_i(two_f_fp, denom)? - } else { - fp_div_i(f, vega_x)? + let new_x = (x_u as i128).checked_sub(step)?; + if new_x > (x_lo as i128) && new_x < (x_hi as i128) { + Some(new_x as u128) + } else { + None + } }; - - let new_x = x_u as i128 - step; - Ok(if new_x > (x_lo as i128) && new_x < (x_hi as i128) { - new_x as u128 - } else { - (x_lo + x_hi) / 2 - }) + Ok(halley_step().unwrap_or(bisect)) } - /// V1 implied volatility: Li rational guess + bracketed Halley iteration. /// Retained as fallback. See `implied_vol` for the production entry point. #[inline(never)] #[allow(dead_code)] -pub(crate) fn implied_vol_v1(market_price: u128, s: u128, k: u128, r: u128, t: u128) -> Result { +pub(crate) fn implied_vol_v1( + market_price: u128, + s: u128, + k: u128, + r: u128, + t: u128, +) -> Result { if s == 0 || k == 0 || t == 0 || market_price == 0 { return Err(SolMathError::DomainError); } @@ -253,7 +297,7 @@ pub(crate) fn implied_vol_v1(market_price: u128, s: u128, k: u128, r: u128, t: u let sqrt_t = fp_sqrt(t)? as i128; let ln_sk = ln_fixed_i(fp_div(s, k)?)?; // ln_sk, r_t both in (~-40·SCALE_I, ~40·SCALE_I) for practical inputs; sum fits i128. - let ln_fk = ln_sk + r_t; // x = ln(F/K) + let ln_fk = ln_sk.checked_add(r_t).ok_or(SolMathError::Overflow)?; // x = ln(F/K) // ---- Li normalization: c = C/S ---- let c_raw = fp_div_i(mp_i, s_i)?; @@ -312,7 +356,11 @@ pub(crate) fn implied_vol_v1(market_price: u128, s: u128, k: u128, r: u128, t: u let target_i = if solve_as_put { // mp_i, s_i, k_disc all < ~1e17; differences fit i128. let put_i = mp_i - s_i + k_disc; - if put_i > 0 { put_i } else { 1 } + if put_i > 0 { + put_i + } else { + 1 + } } else { mp_i }; @@ -326,9 +374,12 @@ pub(crate) fn implied_vol_v1(market_price: u128, s: u128, k: u128, r: u128, t: u for _ in 0..4u8 { let x_i = x_u as i128; - if x_i <= 1 { break; } + if x_i <= 1 { + break; + } - let (price_i, vega_x, volga_x) = iv_price_and_greeks(x_i, ln_fk, s_i, k_disc, solve_as_put)?; + let (price_i, vega_x, volga_x) = + iv_price_and_greeks(x_i, ln_fk, s_i, k_disc, solve_as_put)?; let f = price_i - target_i; if f.abs() <= 100 { @@ -341,9 +392,13 @@ pub(crate) fn implied_vol_v1(market_price: u128, s: u128, k: u128, r: u128, t: u // Tighten bracket if f > 0 { - if x_u < x_hi { x_hi = x_u; } + if x_u < x_hi { + x_hi = x_u; + } } else { - if x_u > x_lo { x_lo = x_u; } + if x_u > x_lo { + x_lo = x_u; + } } x_u = halley_step_bracketed(x_u, f, vega_x, volga_x, x_lo, x_hi)?; @@ -353,9 +408,12 @@ pub(crate) fn implied_vol_v1(market_price: u128, s: u128, k: u128, r: u128, t: u // This catches cases where Li's guess was in-domain but needed more iterations. for _ in 0..4u8 { let x_i = x_u as i128; - if x_i <= 1 { break; } + if x_i <= 1 { + break; + } - let (price_i, vega_x, volga_x) = iv_price_and_greeks(x_i, ln_fk, s_i, k_disc, solve_as_put)?; + let (price_i, vega_x, volga_x) = + iv_price_and_greeks(x_i, ln_fk, s_i, k_disc, solve_as_put)?; let f = price_i - target_i; if f.abs() <= 100 { @@ -367,9 +425,13 @@ pub(crate) fn implied_vol_v1(market_price: u128, s: u128, k: u128, r: u128, t: u } if f > 0 { - if x_u < x_hi { x_hi = x_u; } + if x_u < x_hi { + x_hi = x_u; + } } else { - if x_u > x_lo { x_lo = x_u; } + if x_u > x_lo { + x_lo = x_u; + } } x_u = halley_step_bracketed(x_u, f, vega_x, volga_x, x_lo, x_hi)?; @@ -387,12 +449,20 @@ pub(crate) fn implied_vol_v1(market_price: u128, s: u128, k: u128, r: u128, t: u // Still didn't converge — fall through to Jaeckel for a fresh start implied_vol_iterative( - market_price, s, k, r, t, - r_t, discount, k_disc, sqrt_t, ln_sk, ln_fk, + market_price, + s, + k, + r, + t, + r_t, + discount, + k_disc, + sqrt_t, + ln_sk, + ln_fk, ) } - /// Iterative IV fallback: Jaeckel initial guess + 6 bracketed Halley iterations. /// /// Returns `Err(NoConvergence)` if the solver doesn't converge — never returns @@ -421,7 +491,11 @@ pub(crate) fn implied_vol_iterative( let target_i = if solve_as_put { // mp_i, s_i, k_disc all < ~1e17; put_i fits i128. let put_i = mp_i - s_i + k_disc; - if put_i > 0 { put_i } else { 1 } + if put_i > 0 { + put_i + } else { + 1 + } } else { mp_i }; @@ -487,7 +561,11 @@ pub(crate) fn implied_vol_iterative( // exp_neg_half_x ∈ (0, SCALE_I]; /2 gives positive value well within i128. // mul_fast(phi_neg_sc, exp_half_x): phi_neg_sc ∈ [0,SCALE_I], exp_half_x ≥ SCALE_I/2; product fits via mul_fast. let v = exp_neg_half_x / 2 - mul_fast(phi_neg_sc, exp_half_x); - if v > 0 { v } else { 0 } + if v > 0 { + v + } else { + 0 + } }; // INV_SQRT_2PI ≈ 0.4·SCALE_I; exp_neg_half_x ≤ SCALE_I; mul_fast result < 0.4·SCALE_I. Fits i128. let v_c = mul_fast(INV_SQRT_2PI, exp_neg_half_x); @@ -512,9 +590,12 @@ pub(crate) fn implied_vol_iterative( for iter in 0..6u8 { let x_i = x_u as i128; - if x_i <= 1 { break; } + if x_i <= 1 { + break; + } - let (price_i, vega_x, volga_x) = iv_price_and_greeks(x_i, ln_fk, s_i, k_disc, solve_as_put)?; + let (price_i, vega_x, volga_x) = + iv_price_and_greeks(x_i, ln_fk, s_i, k_disc, solve_as_put)?; let f = price_i - target_i; if f.abs() <= 1 { @@ -536,9 +617,13 @@ pub(crate) fn implied_vol_iterative( // Tighten bracket if f > 0 { - if x_u < x_hi { x_hi = x_u; } + if x_u < x_hi { + x_hi = x_u; + } } else { - if x_u > x_lo { x_lo = x_u; } + if x_u > x_lo { + x_lo = x_u; + } } x_u = halley_step_bracketed(x_u, f, vega_x, volga_x, x_lo, x_hi)?; @@ -597,8 +682,8 @@ fn normalised_black_call(x: i128, s: i128) -> Result { // h = x/s SCALE-valued; t = s/2 ≤ SCALE_I/2; h+t and h-t each have magnitude < ~SCALE_I. Fits i128. // mul_fast operands: norm_cdf_poly ∈ [0, SCALE_I], exp_half and inv_exp_half are SCALE-valued outputs; product fits. // Subtraction of two mul_fast results (both < ~SCALE_I): fits i128. - let b = mul_fast(norm_cdf_poly(h + t)?, exp_half) - - mul_fast(norm_cdf_poly(h - t)?, inv_exp_half); + let b = + mul_fast(norm_cdf_poly(h + t)?, exp_half) - mul_fast(norm_cdf_poly(h - t)?, inv_exp_half); Ok(if b > 0 { b } else { 0 }) } @@ -613,13 +698,22 @@ fn normalised_vega(x: i128, s: i128) -> Result { return Ok(0); // s far too small relative to |x| } let h = fp_div_i(x, s)?; - // s is a SCALE-valued total vol; s/2 ≤ SCALE_I/2. Fits i128. let t = s / 2; - // h = x/s: checked (h can be large for deep ITM/OTM). - // t = s/2 ≤ SCALE_I/2 by construction → t² ≤ SCALE_I/4; fp_mul_i_fast safe. - // INV_SQRT_2PI ≤ SCALE_I, e ≤ SCALE_I → fp_mul_i_fast safe. - let arg = -(fp_mul_i(h, h)? + fp_mul_i_fast(t, t)) / 2; + // For sane vols t = s/2 is small, but a degenerate solver bracket can drive s + // toward the i128::MAX/2 sentinel (see jaeckel_normalised_iv). A huge argument + // means arg → -∞ and vega → 0, so map any overflow of the squared terms — or + // their sum — to an underflow to zero instead of panicking on the former + // unchecked multiply. In the ordinary domain this is bit-for-bit identical. + let sq_sum = match (fp_mul_i(h, h), fp_mul_i(t, t)) { + (Ok(hh), Ok(tt)) => match hh.checked_add(tt) { + Some(v) => v, + None => return Ok(0), + }, + _ => return Ok(0), + }; + let arg = -sq_sum / 2; let e = exp_fixed_i(arg)?; + // INV_SQRT_2PI ≤ SCALE_I and e ≤ SCALE_I, so this product cannot overflow. Ok(fp_mul_i_fast(INV_SQRT_2PI, e)) } @@ -643,8 +737,14 @@ fn householder_factor(newton: i128, halley: i128, hh3: i128) -> Result Result { // x_l, x_r are normalised black call values or s values, all SCALE-valued; difference fits i128. let h = x_r - x_l; @@ -653,7 +753,8 @@ fn rational_cubic_interpolation( return Ok((y_l + y_r) / 2); } // Large r → linear interpolation - if r > 1_000_000_000_000_000_000 { // 1e6 at SCALE + if r > 1_000_000_000_000_000_000 { + // 1e6 at SCALE let t = fp_div_i(x - x_l, h)?; // t ∈ [0, SCALE_I]; y_r,y_l SCALE-valued; two mul_fast terms summed < ~2·SCALE_I. Fits i128. return Ok(mul_fast(y_r, t) + mul_fast(y_l, SCALE_I - t)); @@ -683,44 +784,79 @@ fn rational_cubic_interpolation( /// Control parameter to fit second derivative at left side. fn rc_param_fit_2nd_deriv_left( - x_l: i128, x_r: i128, y_l: i128, y_r: i128, - d_l: i128, d_r: i128, second_deriv: i128, + x_l: i128, + x_r: i128, + y_l: i128, + y_r: i128, + d_l: i128, + d_r: i128, + second_deriv: i128, ) -> Result { // x_l, x_r are SCALE-valued black call outputs; difference fits i128. let h = x_r - x_l; // mul_fast(h, second_deriv): both SCALE-valued, product via mul_fast < ~SCALE_I. /2 and + (d_r - d_l): sum fits i128. let num = mul_fast(h, second_deriv) / 2 + (d_r - d_l); - if num.abs() < 100 { return Ok(0); } + if num.abs() < 100 { + return Ok(0); + } let slope = if h == 0 { 0 } else { fp_div_i(y_r - y_l, h)? }; // slope, d_l both SCALE-valued; difference fits i128. let den = slope - d_l; if den.abs() < 100 { - return Ok(if num > 0 { 1_000_000_000_000_000_000 } else { -SCALE_I + 1 }); + return Ok(if num > 0 { + 1_000_000_000_000_000_000 + } else { + -SCALE_I + 1 + }); + } + if den == 0 { + Ok(0) + } else { + fp_div_i(num, den) } - if den == 0 { Ok(0) } else { fp_div_i(num, den) } } /// Control parameter to fit second derivative at right side. fn rc_param_fit_2nd_deriv_right( - x_l: i128, x_r: i128, y_l: i128, y_r: i128, - d_l: i128, d_r: i128, second_deriv: i128, + x_l: i128, + x_r: i128, + y_l: i128, + y_r: i128, + d_l: i128, + d_r: i128, + second_deriv: i128, ) -> Result { // Same bounds as left variant: h, num, and den all SCALE-valued; fit i128. let h = x_r - x_l; let num = mul_fast(h, second_deriv) / 2 + (d_r - d_l); - if num.abs() < 100 { return Ok(0); } + if num.abs() < 100 { + return Ok(0); + } let slope = if h == 0 { 0 } else { fp_div_i(y_r - y_l, h)? }; let den = d_r - slope; if den.abs() < 100 { - return Ok(if num > 0 { 1_000_000_000_000_000_000 } else { -SCALE_I + 1 }); + return Ok(if num > 0 { + 1_000_000_000_000_000_000 + } else { + -SCALE_I + 1 + }); + } + if den == 0 { + Ok(0) + } else { + fp_div_i(num, den) } - if den == 0 { Ok(0) } else { fp_div_i(num, den) } } const MIN_RC_PARAM: i128 = -SCALE_I + 1; // -(1 - ε) /// Minimum control parameter for shape preservation. -fn minimum_rc_param(d_l: i128, d_r: i128, s: i128, prefer_shape: bool) -> Result { +fn minimum_rc_param( + d_l: i128, + d_r: i128, + s: i128, + prefer_shape: bool, +) -> Result { let monotonic = (mul_fast(d_l, s) >= 0) && (mul_fast(d_r, s) >= 0); let convex = d_l <= s && s <= d_r; let concave = d_l >= s && s >= d_r; @@ -742,8 +878,16 @@ fn minimum_rc_param(d_l: i128, d_r: i128, s: i128, prefer_shape: bool) -> Result let dr_m_s = d_r - s; let dr_m_dl = d_r - d_l; if s_m_dl.abs() > 100 && dr_m_s.abs() > 100 { - let r2a = if dr_m_s == 0 { 0 } else { fp_div_i(dr_m_dl, dr_m_s)?.abs() }; - let r2b = if s_m_dl == 0 { 0 } else { fp_div_i(dr_m_dl, s_m_dl)?.abs() }; + let r2a = if dr_m_s == 0 { + 0 + } else { + fp_div_i(dr_m_dl, dr_m_s)?.abs() + }; + let r2b = if s_m_dl == 0 { + 0 + } else { + fp_div_i(dr_m_dl, s_m_dl)?.abs() + }; r2 = r2a.max(r2b); } else if prefer_shape { r2 = 1_000_000_000_000_000_000; @@ -756,8 +900,14 @@ fn minimum_rc_param(d_l: i128, d_r: i128, s: i128, prefer_shape: bool) -> Result /// Convex control parameter fitting 2nd derivative at left side. fn convex_rc_param_left( - x_l: i128, x_r: i128, y_l: i128, y_r: i128, - d_l: i128, d_r: i128, second_deriv: i128, prefer_shape: bool, + x_l: i128, + x_r: i128, + y_l: i128, + y_r: i128, + d_l: i128, + d_r: i128, + second_deriv: i128, + prefer_shape: bool, ) -> Result { let r = rc_param_fit_2nd_deriv_left(x_l, x_r, y_l, y_r, d_l, d_r, second_deriv)?; let h = x_r - x_l; @@ -768,8 +918,14 @@ fn convex_rc_param_left( /// Convex control parameter fitting 2nd derivative at right side. fn convex_rc_param_right( - x_l: i128, x_r: i128, y_l: i128, y_r: i128, - d_l: i128, d_r: i128, second_deriv: i128, prefer_shape: bool, + x_l: i128, + x_r: i128, + y_l: i128, + y_r: i128, + d_l: i128, + d_r: i128, + second_deriv: i128, + prefer_shape: bool, ) -> Result { let r = rc_param_fit_2nd_deriv_right(x_l, x_r, y_l, y_r, d_l, d_r, second_deriv)?; let h = x_r - x_l; @@ -797,23 +953,37 @@ fn compute_f_lower_map(x: i128, s: i128) -> Result<(i128, i128, i128), SolMathEr let exp_y_s2 = exp_fixed_i(y + s2 / 8)?; // Nested mul_fast: each result ≤ SCALE_I; TWO_PI_SCALED ≈ 6.28·SCALE_I; outermost < ~6.28·SCALE_I. Fits i128. let fp = mul_fast(TWO_PI_SCALED, mul_fast(y, mul_fast(phi2, exp_y_s2))); - let f = if ax < 100 { 0 } else { + let f = if ax < 100 { + 0 + } else { // TWO_PI_OVER_SQRT_TWENTY_SEVEN ≈ 1.21·SCALE_I; ax and inner mul_fast ≤ SCALE_I; product < ~1.21·SCALE_I. - mul_fast(TWO_PI_OVER_SQRT_TWENTY_SEVEN, mul_fast(ax, mul_fast(phi2, phi))) + mul_fast( + TWO_PI_OVER_SQRT_TWENTY_SEVEN, + mul_fast(ax, mul_fast(phi2, phi)), + ) }; // fpp (second derivative) — simplified, only used for control parameter // 2*y ≤ ~0.67·SCALE_I; s2/4 ≤ ~0.25·SCALE_I; sum < SCALE_I. Fits i128. let exp_2y_s2 = exp_fixed_i(2 * y + s2 / 4)?; - let fpp = if pdf.abs() < 100 { 0 } else { + let fpp = if pdf.abs() < 100 { + 0 + } else { // 8·SQRT_THREE_SCALED ≈ 13.9·SCALE_I; mul_fast(s, ax) ≤ SCALE_I; first mul_fast < ~13.9·SCALE_I. // s2 - 8·SCALE_I fits i128; mul_fast(s2, s2-8·SCALE_I) ≤ SCALE_I; 3× ≤ 3·SCALE_I. // 8·mul_fast(x,x): x ≤ SCALE_I, mul_fast(x,x) ≤ SCALE_I; ×8 ≤ 8·SCALE_I. // Subtracting: |inner addend| < ~11·SCALE_I; sum < ~25·SCALE_I. Fits i128. let inner = mul_fast(8 * SQRT_THREE_SCALED, mul_fast(s, ax)) - + mul_fast(3 * mul_fast(s2, s2 - 8 * SCALE_I) - 8 * mul_fast(x, x), - fp_div_i(phi, pdf)?); - mul_fast(PI_OVER_SIX, mul_fast(fp_div_i(y, mul_fast(s2, s))?, - mul_fast(phi, mul_fast(inner, exp_2y_s2)))) + + mul_fast( + 3 * mul_fast(s2, s2 - 8 * SCALE_I) - 8 * mul_fast(x, x), + fp_div_i(phi, pdf)?, + ); + mul_fast( + PI_OVER_SIX, + mul_fast( + fp_div_i(y, mul_fast(s2, s))?, + mul_fast(phi, mul_fast(inner, exp_2y_s2)), + ), + ) }; Ok((f, fp, fpp)) } @@ -833,9 +1003,10 @@ fn compute_f_upper_map(x: i128, s: i128) -> Result<(i128, i128, i128), SolMathEr // s*s: s is SCALE-valued ≤ SCALE_I, but raw s*s could be up to SCALE_I² ≈ 1e24. // REVIEW: s can be up to ~10·SCALE_I for high-vol inputs; s*s up to ~1e26 fits i128 (i128::MAX ≈ 1.7e38), // then /8000_000_000_000 = /8e12 gives ≤ ~1.25e13. w + that ≤ ~1.25e13 + SCALE_I. Fits i128. - let fpp = mul_fast(SQRT_PI_OVER_TWO, - mul_fast(exp_fixed_i(w + s * s / 8000_000_000_000)?, - fp_div_i(w, s)?)); + let fpp = mul_fast( + SQRT_PI_OVER_TWO, + mul_fast(exp_fixed_i(w + s * s / 8000_000_000_000)?, fp_div_i(w, s)?), + ); Ok((f, fp, fpp)) } @@ -900,20 +1071,26 @@ fn jaeckel_normalised_iv(beta: i128, x: i128, n_householder: u8) -> Result 100 { s_c - fp_div_i(b_c, v_c)? } else { s_c / 2 }; + let s_l = if v_c > 100 { + s_c - fp_div_i(b_c, v_c)? + } else { + s_c / 2 + }; let s_l = if s_l > 0 { s_l } else { s_c / 10 }; let b_l = normalised_black_call(x, s_l)?; if beta < b_l { // Branch 1: extreme OTM — f_lower_map inverse let (f_l, dfdb_l, d2fdb2_l) = compute_f_lower_map(x, s_l)?; - let r_ll = convex_rc_param_right( - 0, b_l, 0, f_l, SCALE_I, dfdb_l, d2fdb2_l, true)?; - let mut f = rational_cubic_interpolation( - beta, 0, b_l, 0, f_l, SCALE_I, dfdb_l, r_ll)?; + let r_ll = convex_rc_param_right(0, b_l, 0, f_l, SCALE_I, dfdb_l, d2fdb2_l, true)?; + let mut f = rational_cubic_interpolation(beta, 0, b_l, 0, f_l, SCALE_I, dfdb_l, r_ll)?; if f <= 0 { // Quadratic fallback - let t = if b_l > 0 { fp_div_i(beta, b_l)? } else { SCALE_I / 2 }; + let t = if b_l > 0 { + fp_div_i(beta, b_l)? + } else { + SCALE_I / 2 + }; // t ∈ [0, SCALE_I]; f_l, b_l SCALE-valued; two mul_fast terms < ~SCALE_I; sum < ~2·SCALE_I. // Outer mul_fast by t ≤ SCALE_I: result ≤ ~2·SCALE_I. Fits i128. f = mul_fast(mul_fast(f_l, t) + mul_fast(b_l, SCALE_I - t), t); @@ -924,12 +1101,18 @@ fn jaeckel_normalised_iv(beta: i128, x: i128, n_householder: u8) -> Result 100 { fp_div_i(SCALE_I, v_l)? } else { SCALE_I * 100 }; - let inv_v_c = if v_c > 100 { fp_div_i(SCALE_I, v_c)? } else { SCALE_I * 100 }; - let r_lm = convex_rc_param_right( - b_l, b_c, s_l, s_c, inv_v_l, inv_v_c, 0, false)?; - s = rational_cubic_interpolation( - beta, b_l, b_c, s_l, s_c, inv_v_l, inv_v_c, r_lm)?; + let inv_v_l = if v_l > 100 { + fp_div_i(SCALE_I, v_l)? + } else { + SCALE_I * 100 + }; + let inv_v_c = if v_c > 100 { + fp_div_i(SCALE_I, v_c)? + } else { + SCALE_I * 100 + }; + let r_lm = convex_rc_param_right(b_l, b_c, s_l, s_c, inv_v_l, inv_v_c, 0, false)?; + s = rational_cubic_interpolation(beta, b_l, b_c, s_l, s_c, inv_v_l, inv_v_c, r_lm)?; s_left = s_l; s_right = s_c; } @@ -947,25 +1130,35 @@ fn jaeckel_normalised_iv(beta: i128, x: i128, n_householder: u8) -> Result 100 { fp_div_i(SCALE_I, v_c)? } else { SCALE_I * 100 }; - let inv_v_h = if v_h > 100 { fp_div_i(SCALE_I, v_h)? } else { SCALE_I * 100 }; - let r_hm = convex_rc_param_left( - b_c, b_h, s_c, s_h, inv_v_c, inv_v_h, 0, false)?; - s = rational_cubic_interpolation( - beta, b_c, b_h, s_c, s_h, inv_v_c, inv_v_h, r_hm)?; + let inv_v_c = if v_c > 100 { + fp_div_i(SCALE_I, v_c)? + } else { + SCALE_I * 100 + }; + let inv_v_h = if v_h > 100 { + fp_div_i(SCALE_I, v_h)? + } else { + SCALE_I * 100 + }; + let r_hm = convex_rc_param_left(b_c, b_h, s_c, s_h, inv_v_c, inv_v_h, 0, false)?; + s = rational_cubic_interpolation(beta, b_c, b_h, s_c, s_h, inv_v_c, inv_v_h, r_hm)?; s_left = s_c; s_right = s_h; } else { // Branch 4: extreme ITM — f_upper_map inverse let (f_h, dfdb_h, d2fdb2_h) = compute_f_upper_map(x, s_h)?; - let r_hh = convex_rc_param_left( - b_h, b_max, f_h, 0, dfdb_h, -SCALE_I / 2, d2fdb2_h, true)?; - let mut f = rational_cubic_interpolation( - beta, b_h, b_max, f_h, 0, dfdb_h, -SCALE_I / 2, r_hh)?; + let r_hh = + convex_rc_param_left(b_h, b_max, f_h, 0, dfdb_h, -SCALE_I / 2, d2fdb2_h, true)?; + let mut f = + rational_cubic_interpolation(beta, b_h, b_max, f_h, 0, dfdb_h, -SCALE_I / 2, r_hh)?; if f <= 0 { // b_max, b_h both SCALE-valued outputs of normalised_black_call; difference fits i128. let h = b_max - b_h; - let t = if h > 0 { fp_div_i(beta - b_h, h)? } else { SCALE_I / 2 }; + let t = if h > 0 { + fp_div_i(beta - b_h, h)? + } else { + SCALE_I / 2 + }; // f_h SCALE-valued; SCALE_I-t ∈ [0,SCALE_I]; mul_fast < SCALE_I. // mul_fast(h, t)/2 < SCALE_I/2. Sum < ~1.5·SCALE_I; outer mul_fast < ~1.5·SCALE_I. Fits i128. f = mul_fast(mul_fast(f_h, SCALE_I - t) + mul_fast(h, t) / 2, SCALE_I - t); @@ -980,21 +1173,30 @@ fn jaeckel_normalised_iv(beta: i128, x: i128, n_householder: u8) -> Result beta && s < s_right { s_right = s; } - else if b < beta && s > s_left { s_left = s; } + if b > beta && s < s_right { + s_right = s; + } else if b < beta && s > s_left { + s_left = s; + } if bp <= 100 { // Near-zero vega — bisect; s_left + s_right < i128::MAX (s_right = i128::MAX/2 at most). /2 fits. @@ -1010,12 +1212,11 @@ fn jaeckel_normalised_iv(beta: i128, x: i128, n_householder: u8) -> Result Result Result Result Result Result 0 { Ok(s) } else { Err(SolMathError::NoConvergence) } + if s > 0 { + Ok(s) + } else { + Err(SolMathError::NoConvergence) + } } /// Iterative initial guess for out-of-Li-domain cases. /// Returns x_vol = σ√T initial estimate. #[inline(never)] fn iterative_initial_guess( - _market_price: u128, s: u128, k: u128, - mp_i: i128, s_i: i128, k_disc: i128, sqrt_t: i128, ln_fk: i128, + _market_price: u128, + s: u128, + k: u128, + mp_i: i128, + s_i: i128, + k_disc: i128, + sqrt_t: i128, + ln_fk: i128, ) -> Result { let abs_ln_fk = ln_fk.abs(); let beta_otm = if ln_fk > 0 { // mp_i, s_i, k_disc < ~1e17; put_i is their signed sum, fits i128. let put_i = mp_i - s_i + k_disc; - if put_i <= 0 { 0 } else { + if put_i <= 0 { + 0 + } else { let sqrt_sk = fp_sqrt(fp_mul(s, k)?)? as i128; fp_div_i(put_i, sqrt_sk)? } @@ -1172,7 +1390,11 @@ fn iterative_initial_guess( let phi_neg_sc = norm_cdf_poly(-s_c)?; // exp_neg_half_x ∈ (0, SCALE_I]; /2 > 0. mul_fast(phi_neg_sc, exp_half_x) ≤ SCALE_I. Difference fits i128. let v = exp_neg_half_x / 2 - mul_fast(phi_neg_sc, exp_half_x); - if v > 0 { v } else { 0 } + if v > 0 { + v + } else { + 0 + } }; // INV_SQRT_2PI ≈ 0.4·SCALE_I; exp_neg_half_x ≤ SCALE_I; mul_fast result < 0.4·SCALE_I. Fits i128. let v_c = mul_fast(INV_SQRT_2PI, exp_neg_half_x); @@ -1204,12 +1426,15 @@ fn iterative_initial_guess( /// /// Uses a Li rational initial guess with bracketed Halley iteration, /// falling back to a Jaeckel normalised-space solver for edge cases. -/// Architecture targets roughly 200K median CU on Solana, but tail cases can -/// require a higher compute budget. +/// Final SBF audit: 179,986 CU average, 170,757 median, 611,909 P99, and +/// 785,327 max over accepted sampled cases. Tail cases require an explicitly +/// raised compute budget. /// /// # Errors /// /// - [`SolMathError::DomainError`] if `s == 0`, `k == 0`, `t == 0`, or `market_price == 0`. +/// Also returned when the call premium violates discounted no-arbitrage +/// bounds. Prices above 100,000 units must be homogeneously rescaled first. /// - [`SolMathError::NoConvergence`] for sub-ULP prices, premiums below 100 /// integer units (1e-10 at `SCALE`), or deep OTM edge cases where no stable /// root can be found. @@ -1235,12 +1460,22 @@ fn iterative_initial_guess( /// # Ok::<(), solmath::SolMathError>(()) /// ``` #[inline(never)] -pub fn implied_vol(market_price: u128, s: u128, k: u128, r: u128, t: u128) -> Result { - // mul_fast inside the solver assumes |s_i|,|k_i| < ~1e17 so products fit i128. - // 1e17 * SCALE = 1e29 — no real token exceeds $100 quadrillion. - const MAX_PRICE: u128 = 170_000_000_000_000 * SCALE; // 1.7e14 * SCALE = 1.7e26 — mul_fast safe - if market_price > i128::MAX as u128 || s > i128::MAX as u128 || k > i128::MAX as u128 - || r > i128::MAX as u128 || t > i128::MAX as u128 +pub fn implied_vol( + market_price: u128, + s: u128, + k: u128, + r: u128, + t: u128, +) -> Result { + // mul_fast is intentionally unchecked and its proof assumes price-like + // values are <= 1e17 raw. Enforce that actual bound; the former cap was + // nine orders of magnitude looser than the invariant in this module. + const MAX_PRICE: u128 = 100_000 * SCALE; + if market_price > i128::MAX as u128 + || s > i128::MAX as u128 + || k > i128::MAX as u128 + || r > i128::MAX as u128 + || t > i128::MAX as u128 { return Err(SolMathError::Overflow); } @@ -1268,11 +1503,15 @@ pub fn implied_vol(market_price: u128, s: u128, k: u128, r: u128, t: u128) -> Re } let ln_sk = ln_fixed_i(fp_div(s, k)?)?; // ln_sk, r_t both in (~-40·SCALE_I, ~40·SCALE_I); sum fits i128. - let ln_fk = ln_sk + r_t; + let ln_fk = ln_sk.checked_add(r_t).ok_or(SolMathError::Overflow)?; // ── V1 setup (~5K CU) ── let discount = exp_fixed_i(-r_t)?; let k_disc = fp_mul_i(k_i, discount)?; + let lower = s_i.saturating_sub(k_disc); + if mp_i < lower || mp_i > s_i { + return Err(SolMathError::DomainError); + } // ── V1 fast path: Li guess + 4 Halley iterations ── let c_raw = fp_div_i(mp_i, s_i)?; @@ -1298,7 +1537,11 @@ pub fn implied_vol(market_price: u128, s: u128, k: u128, r: u128, t: u128) -> Re let target_i = if solve_as_put { // mp_i, s_i, k_disc < ~1e17; put_i fits i128. let put_i = mp_i - s_i + k_disc; - if put_i > 0 { put_i } else { 1 } + if put_i > 0 { + put_i + } else { + 1 + } } else { mp_i }; @@ -1310,7 +1553,9 @@ pub fn implied_vol(market_price: u128, s: u128, k: u128, r: u128, t: u128) -> Re for iter in 0..4u8 { let x_i = x_u as i128; - if x_i <= 1 { break; } + if x_i <= 1 { + break; + } let (price_i, vega_x, volga_x) = iv_price_and_greeks(x_i, ln_fk, s_i, k_disc, solve_as_put)?; @@ -1323,9 +1568,13 @@ pub fn implied_vol(market_price: u128, s: u128, k: u128, r: u128, t: u128) -> Re } if f > 0 { - if x_u < x_hi { x_hi = x_u; } + if x_u < x_hi { + x_hi = x_u; + } } else { - if x_u > x_lo { x_lo = x_u; } + if x_u > x_lo { + x_lo = x_u; + } } x_u = halley_step_bracketed(x_u, f, vega_x, volga_x, x_lo, x_hi)?; @@ -1351,14 +1600,16 @@ pub fn implied_vol(market_price: u128, s: u128, k: u128, r: u128, t: u128) -> Re let target_i = if solve_as_put { // mp_i, s_i, k_disc < ~1e17; sum fits i128. let put_i = mp_i - s_i + k_disc; - if put_i > 0 { put_i } else { 1 } + if put_i > 0 { + put_i + } else { + 1 + } } else { mp_i }; - let x_vol = iterative_initial_guess( - market_price, s, k, mp_i, s_i, k_disc, sqrt_t, ln_fk, - )?; + let x_vol = iterative_initial_guess(market_price, s, k, mp_i, s_i, k_disc, sqrt_t, ln_fk)?; // 1e9·sqrt_t and 1e13·sqrt_t: both safe via mul_fast (same bound as other bracket sites). let x_min = (mul_fast(1_000_000_000, sqrt_t)).max(1) as u128; @@ -1369,7 +1620,9 @@ pub fn implied_vol(market_price: u128, s: u128, k: u128, r: u128, t: u128) -> Re for iter in 0..5u8 { let x_i = x_u as i128; - if x_i <= 1 { break; } + if x_i <= 1 { + break; + } let (price_i, vega_x, volga_x) = iv_price_and_greeks(x_i, ln_fk, s_i, k_disc, solve_as_put)?; @@ -1383,9 +1636,13 @@ pub fn implied_vol(market_price: u128, s: u128, k: u128, r: u128, t: u128) -> Re } if f > 0 { - if x_u < x_hi { x_hi = x_u; } + if x_u < x_hi { + x_hi = x_u; + } } else { - if x_u > x_lo { x_lo = x_u; } + if x_u > x_lo { + x_lo = x_u; + } } x_u = halley_step_bracketed(x_u, f, vega_x, volga_x, x_lo, x_hi)?; @@ -1394,8 +1651,7 @@ pub fn implied_vol(market_price: u128, s: u128, k: u128, r: u128, t: u128) -> Re // Final check at 2000 ULP — wider than path A to avoid expensive Jäckel fallback let x_i = x_u as i128; if x_i > 0 { - let (price_i, _, _) = - iv_price_and_greeks(x_i, ln_fk, s_i, k_disc, solve_as_put)?; + let (price_i, _, _) = iv_price_and_greeks(x_i, ln_fk, s_i, k_disc, solve_as_put)?; if (price_i - target_i).abs() <= 2000 { return Ok(fp_div_i(x_i, sqrt_t)? as u128); } @@ -1427,7 +1683,11 @@ pub fn implied_vol(market_price: u128, s: u128, k: u128, r: u128, t: u128) -> Re let intrinsic = forward - k_i; // undiscounted_call, intrinsic both < ~1e17; difference fits i128. let put_undiscounted = undiscounted_call - (if intrinsic > 0 { intrinsic } else { 0 }); - let put_undiscounted = if put_undiscounted > 0 { put_undiscounted } else { 0 }; + let put_undiscounted = if put_undiscounted > 0 { + put_undiscounted + } else { + 0 + }; (-x, fp_div_i(put_undiscounted, sqrt_fk)?) } else { // mp_i < ~1e17; exp_rt SCALE-valued; mul_fast result < ~1e17. Fits i128. @@ -1467,7 +1727,13 @@ pub fn implied_vol(market_price: u128, s: u128, k: u128, r: u128, t: u128) -> Re /// Uses 2 Householder iterations. Not CU-constrained. #[inline(never)] #[allow(dead_code)] -pub(crate) fn implied_vol_jaeckel(market_price: u128, s: u128, k: u128, r: u128, t: u128) -> Result { +pub(crate) fn implied_vol_jaeckel( + market_price: u128, + s: u128, + k: u128, + r: u128, + t: u128, +) -> Result { let s_i = s as i128; let k_i = k as i128; let r_i = r as i128; @@ -1505,7 +1771,11 @@ pub(crate) fn implied_vol_jaeckel(market_price: u128, s: u128, k: u128, r: u128, let intrinsic = forward - k_i; // undiscounted_call, intrinsic < ~1e17; difference fits i128. let put_undiscounted = undiscounted_call - (if intrinsic > 0 { intrinsic } else { 0 }); - let put_undiscounted = if put_undiscounted > 0 { put_undiscounted } else { 0 }; + let put_undiscounted = if put_undiscounted > 0 { + put_undiscounted + } else { + 0 + }; (-x, fp_div_i(put_undiscounted, sqrt_fk)?) } else { // mp_i < ~1e17; exp_rt SCALE-valued; mul_fast result < ~1e17. Fits i128. @@ -1539,3 +1809,20 @@ pub(crate) fn implied_vol_jaeckel(market_price: u128, s: u128, k: u128, r: u128, Err(SolMathError::NoConvergence) } + +#[cfg(test)] +mod adversarial_tests { + use super::*; + + #[test] + fn advertised_cap_cannot_reach_unchecked_multiply_overflow() { + let s = 100_000_000 * SCALE; + let call = black_scholes_price(s, s, 0, 200_000_000_000, SCALE) + .unwrap() + .0; + assert_eq!( + implied_vol(call, s, s, 0, SCALE), + Err(SolMathError::Overflow) + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 95d8576..88bdcb9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,11 @@ //! # solmath //! -//! Fixed-point financial math for Solana. Black-Scholes, Greeks, implied volatility, -//! fat-tail pricing, and weighted pool math — all in pure integer arithmetic. +//! Deterministic fixed-point mathematics and quantitative finance for Solana +//! and `no_std` Rust. The crate combines checked decimal arithmetic, +//! transcendentals, probability functions, Black-Scholes and Greeks, implied +//! volatility, barriers, arithmetic-Asian/TWAP settlement, American KBI, +//! exponential NIG, two-asset rainbow options, deterministic Heston, SABR, +//! and weighted-pool math behind one dependency-free API. //! //! All values use `u128` or `i128` scaled by 1e12 (12 decimal places). //! HP variants use 1e15 internally but accept and return 1e12 values. @@ -29,7 +33,8 @@ //! # } //! ``` //! -//! Agrees with QuantLib's AnalyticEuropeanEngine to 10-14 significant figures. +//! The HP Black-Scholes path agrees with QuantLib's AnalyticEuropeanEngine to +//! roughly 10-14 significant figures on non-tiny outputs in the reference corpus. #![forbid(unsafe_code)] #![no_std] @@ -39,26 +44,38 @@ extern crate alloc; extern crate std; // Core — always compiled -mod constants; -pub mod error; pub mod arithmetic; -pub mod overflow; -pub mod mul_div; +mod constants; pub mod double_word; pub mod encoding; +pub mod error; +#[cfg(feature = "transcendental")] +mod exp_coeffs; +#[cfg(feature = "transcendental")] +mod expm1_lut; +#[cfg(feature = "transcendental")] +mod ln2_lut; +#[cfg(feature = "transcendental")] +mod ln_lut; +#[cfg(feature = "transcendental")] +mod lut_budget; +pub mod mul_div; +#[cfg(feature = "transcendental")] +mod norm_cdf_coeffs; +pub mod overflow; mod utils; // Transcendental bundle #[cfg(feature = "transcendental")] -pub mod transcendental; -#[cfg(feature = "transcendental")] -pub mod trig; +pub mod hp; +#[cfg(feature = "nig")] +pub mod i64_math; #[cfg(feature = "transcendental")] pub mod normal; #[cfg(feature = "transcendental")] -pub mod hp; +pub mod transcendental; #[cfg(feature = "transcendental")] -pub mod i64_math; +pub mod trig; // Complex arithmetic #[cfg(feature = "complex")] @@ -72,18 +89,44 @@ pub mod bs; #[cfg(feature = "iv")] pub mod iv; +// Safe-by-construction validated pricing inputs +#[cfg(any( + feature = "bs", + feature = "barrier", + feature = "asian", + feature = "pool" +))] +pub mod checked; + +// Kim Boundary Integration (KBI): nonlinear exercise-boundary reconstruction +// followed by Kim's early-exercise-premium integral. +#[cfg(feature = "american-kbi")] +pub mod american_kbi; +#[cfg(feature = "american-kbi")] +#[allow(dead_code)] +mod american_kbi_data; + +// Two-asset rainbow options +#[cfg(feature = "rainbow")] +pub mod rainbow; + // Barrier options #[cfg(feature = "barrier")] pub mod barrier; -// NIG fat-tail pricing +// Continuous arithmetic-Asian / partially fixed TWAP options +#[cfg(feature = "asian")] +pub mod asian; + +// Exponential NIG pricing on an explicitly bounded production domain. #[cfg(feature = "nig")] pub mod nig; -// Heston stochastic volatility +// Deterministic Heston limit (`xi = 0`). #[cfg(feature = "heston")] pub mod heston; -#[cfg(feature = "heston")] +// Test-only characteristic-function research retained outside production code. +#[cfg(all(feature = "heston", feature = "complex", test))] mod i64_cf; // SABR stochastic volatility @@ -103,88 +146,121 @@ pub mod phi2table; // ── Public API ── // Constants & types -pub use constants::{SCALE, SCALE_I, BsFull, LN2_LO, LN2_HP_LO, LN_REMEZ_COEFFS, LN_REMEZ_HP_COEFFS}; +pub use constants::{ + BsFull, LN2_HP_LO, LN2_LO, LN_REMEZ_COEFFS, LN_REMEZ_HP_COEFFS, SCALE, SCALE_I, +}; pub use double_word::DoubleWord; -pub use error::SolMathError; pub use encoding::{fp, fp_i}; +pub use error::SolMathError; // Arithmetic pub use arithmetic::{ - fp_mul, fp_mul_i, fp_mul_round, fp_mul_i_round, fp_mul_i_round_dw, - fp_div, fp_div_i, fp_div_round, - fp_div_floor, fp_div_ceil, - fp_sqrt, -}; -pub use overflow::{ - checked_mul_div_i, - checked_mul_div_floor_i, - checked_mul_div_ceil_i, + fp_div, fp_div_ceil, fp_div_floor, fp_div_i, fp_div_round, fp_mul, fp_mul_i, fp_mul_i_round, + fp_mul_i_round_dw, fp_mul_round, fp_sqrt, }; +pub use overflow::{checked_mul_div_ceil_i, checked_mul_div_floor_i, checked_mul_div_i}; // Integer mul-div (u64, no SCALE) -pub use mul_div::{mul_div_floor, mul_div_ceil, mul_div_floor_u128, mul_div_ceil_u128}; +pub use mul_div::{mul_div_ceil, mul_div_ceil_u128, mul_div_floor, mul_div_floor_u128}; // Transcendentals #[cfg(feature = "transcendental")] -pub use transcendental::{ln_fixed_i, exp_fixed_i, pow_fixed, pow_int, pow_fixed_i, expm1_fixed}; +pub use transcendental::{ + exp_fixed_i, expm1_fixed, ln_1p_fixed, ln_fixed_i, pow_fixed, pow_fixed_i, pow_int, +}; #[cfg(feature = "transcendental")] -pub use trig::{sin_fixed, cos_fixed, sincos_fixed}; +pub use trig::{cos_fixed, sin_fixed, sincos_fixed}; #[cfg(feature = "transcendental")] -pub use normal::{norm_cdf_poly, norm_pdf, norm_cdf_and_pdf, inverse_norm_cdf}; +pub use normal::{ + inverse_norm_cdf, norm_cdf_and_pdf, norm_cdf_and_pdf_poly, norm_cdf_poly, norm_pdf, +}; #[cfg(feature = "transcendental")] pub use hp::{ - ln_fixed_hp, exp_fixed_hp, pow_fixed_hp, pow_product_hp, - norm_cdf_poly_hp, black_scholes_price_hp, bs_full_hp, - fp_mul_hp_i, fp_mul_hp_u, fp_div_hp_safe, + black_scholes_price_hp, bs_full_hp, exp_fixed_hp, fp_div_hp_safe, fp_mul_hp_i, fp_mul_hp_u, + ln_fixed_hp, norm_cdf_poly_hp, pow_fixed_hp, pow_product_hp, }; -#[cfg(feature = "transcendental")] +#[cfg(feature = "nig")] pub use i64_math::{nig_call_64, nig_put_64}; // Complex #[cfg(feature = "complex")] -pub use complex::{Complex, complex_mul, complex_div, complex_exp, complex_sqrt}; +pub use complex::{complex_div, complex_exp, complex_mul, complex_sqrt, Complex}; // Black-Scholes #[cfg(feature = "bs")] -pub use bs::{black_scholes_price, bs_full, bs_delta, bs_gamma, bs_vega, bs_theta, bs_rho}; +pub use bs::{black_scholes_price, bs_delta, bs_full, bs_gamma, bs_rho, bs_theta, bs_vega}; + +// Safe-by-construction validated inputs (recommended program-boundary API) +#[cfg(feature = "barrier")] +pub use checked::BarrierInputs; +#[cfg(feature = "bs")] +pub use checked::EuropeanInputs; +#[cfg(feature = "iv")] +pub use checked::ImpliedVolInputs; +#[cfg(feature = "pool")] +pub use checked::PoolSwapInputs; +#[cfg(feature = "asian")] +pub use checked::TwapInputs; +#[cfg(any(feature = "bs", feature = "barrier", feature = "asian"))] +pub use checked::{Price, Rate, Time, Vol}; // Implied volatility #[cfg(feature = "iv")] pub use iv::implied_vol; +#[cfg(feature = "american-kbi")] +pub use american_kbi::{ + american_kbi_price, AmericanKbiKind, AMERICAN_KBI_ARTIFACT_SHA256, AMERICAN_KBI_NODES, + AMERICAN_KBI_PRICE_POINTS, +}; + +// Rainbow (two-asset) options +#[cfg(feature = "rainbow")] +pub use rainbow::{best_of_call, worst_of_call}; + // Barrier options #[cfg(feature = "barrier")] -pub use barrier::{barrier_option, BarrierType, BarrierResult}; +pub use barrier::{barrier_option, barrier_option_with_state, BarrierResult, BarrierType}; + +// Continuous arithmetic-Asian / partially fixed TWAP options +#[cfg(feature = "asian")] +pub use asian::{arithmetic_asian_price, twap_option_price, AsianOptionResult}; -// NIG fat-tail pricing +// Exponential NIG public pricing API #[cfg(feature = "nig")] -pub use nig::nig_call_price; +pub use nig::{ + nig_call_price, nig_price_certified, CertifiedNigPrice, NigParams, NIG_MAX_ALPHA, + NIG_MAX_DELTA_TIME, NIG_MIN_ALPHA, NIG_MIN_DELTA_TIME, NIG_QUADRATURE_NODES, +}; -// Heston stochastic volatility +// Deterministic Heston API (positive-expiry `xi == 0`). #[cfg(feature = "heston")] pub use heston::heston_price; // SABR stochastic volatility #[cfg(feature = "sabr")] pub use sabr::{ - sabr_implied_vol, sabr_price, sabr_greeks, - SabrSmile, sabr_precompute, sabr_vol_at, - sabr_z_over_chi_pade, + certify_sabr_surface, sabr_greeks, sabr_implied_vol, sabr_precompute, sabr_price, sabr_vol_at, + sabr_z_over_chi_pade, CertifiedSabrQuote, CertifiedSabrSurface, SabrSmile, + MAX_SABR_SURFACE_MATURITIES, MAX_SABR_SURFACE_QUOTES, MAX_SABR_SURFACE_STRIKES, }; // Pool math #[cfg(feature = "pool")] -pub use pool::{weighted_pool_swap, token_to_fp, fp_to_token_floor, fp_to_token_ceil}; +pub use pool::{fp_to_token_ceil, fp_to_token_floor, token_to_fp, weighted_pool_swap}; // Bivariate normal CDF #[cfg(feature = "bivariate")] pub use bvn_cdf::{bvn_cdf, bvn_cdf_hp}; #[cfg(feature = "bivariate")] -pub use phi2table::Phi2Table; +pub use phi2table::{ + CertifiedPhi2Evaluator, Phi2Certificate, Phi2DenseTable, Phi2Interpolation, Phi2Reference, + Phi2Table, PHI2_DENSE_GRID_SIZE, PHI2_GRID_SIZE, PHI2_ROW_DIGEST_BYTES, +}; #[cfg(all(test, feature = "full"))] mod tests { @@ -240,12 +316,18 @@ mod tests { #[test] fn checked_mul_div_i_division_by_zero() { - assert_eq!(checked_mul_div_i(SCALE_I, SCALE_I, 0), Err(SolMathError::DivisionByZero)); + assert_eq!( + checked_mul_div_i(SCALE_I, SCALE_I, 0), + Err(SolMathError::DivisionByZero) + ); } #[test] fn checked_mul_div_i_overflow() { - assert_eq!(checked_mul_div_i(i128::MAX, i128::MAX, 1), Err(SolMathError::Overflow)); + assert_eq!( + checked_mul_div_i(i128::MAX, i128::MAX, 1), + Err(SolMathError::Overflow) + ); } #[test] @@ -315,7 +397,7 @@ mod tests { } #[test] - fn heston_rejects_values_above_i64_cv_representable_range() { + fn heston_stochastic_rejection_does_not_depend_on_private_research_limits() { assert_eq!( heston_price( 1_000_000_000_000_000_000_000_000_000_000u128, @@ -328,7 +410,7 @@ mod tests { SCALE / 2, -700_000_000_000, ), - Err(SolMathError::Overflow) + Err(SolMathError::NoConvergence) ); } @@ -343,7 +425,15 @@ mod tests { #[test] fn sabr_rejects_values_above_i128_range() { assert_eq!( - sabr_implied_vol(i128::MAX as u128 + 1, SCALE, SCALE, SCALE / 5, SCALE / 2, 0, SCALE / 5), + sabr_implied_vol( + i128::MAX as u128 + 1, + SCALE, + SCALE, + SCALE / 5, + SCALE / 2, + 0, + SCALE / 5 + ), Err(SolMathError::Overflow) ); } @@ -363,11 +453,27 @@ mod tests { #[test] fn sabr_rejects_invalid_rho() { assert_eq!( - sabr_implied_vol(SCALE, SCALE, SCALE, SCALE / 5, SCALE / 2, SCALE_I, SCALE / 5), + sabr_implied_vol( + SCALE, + SCALE, + SCALE, + SCALE / 5, + SCALE / 2, + SCALE_I, + SCALE / 5 + ), Err(SolMathError::DomainError) ); assert_eq!( - sabr_implied_vol(SCALE, SCALE, SCALE, SCALE / 5, SCALE / 2, -SCALE_I, SCALE / 5), + sabr_implied_vol( + SCALE, + SCALE, + SCALE, + SCALE / 5, + SCALE / 2, + -SCALE_I, + SCALE / 5 + ), Err(SolMathError::DomainError) ); assert!(matches!( @@ -399,8 +505,13 @@ mod tests { let result = ln_fixed_i(14313).unwrap(); let expected: i128 = -18062097621744; let error = (result - expected).abs(); - assert!(error <= 3, "Regression: expected ≤3 ULP, got {} ULP (result={}, expected={})", - error, result, expected); + assert!( + error <= 2, + "Regression: expected ≤2 ULP, got {} ULP (result={}, expected={})", + error, + result, + expected + ); } #[test] @@ -423,7 +534,11 @@ mod tests { // Just above 1: ln(SCALE + 1) should be ≈ 1/SCALE * SCALE = 1 let ln_near_1 = ln_fixed_i(SCALE + 1).unwrap(); - assert!(ln_near_1 >= 0 && ln_near_1 <= 2, "ln(1+eps) = {}", ln_near_1); + assert!( + ln_near_1 >= 0 && ln_near_1 <= 2, + "ln(1+eps) = {}", + ln_near_1 + ); } #[test] @@ -434,6 +549,7 @@ mod tests { #[test] fn exp_underflow_is_zero() { assert_eq!(exp_fixed_i(-41 * SCALE_I), Ok(0)); + assert_eq!(exp_fixed_i(-40 * SCALE_I), Ok(0)); } #[test] @@ -441,6 +557,21 @@ mod tests { assert_eq!(exp_fixed_i(0), Ok(SCALE_I)); } + #[test] + fn exp_tiny_direct_path_and_seams_are_correctly_rounded() { + assert_eq!(exp_fixed_i(999_999), Ok(SCALE_I + 999_999)); + assert_eq!(exp_fixed_i(-999_999), Ok(SCALE_I - 999_999)); + assert_eq!(exp_fixed_i(1_000_000), Ok(SCALE_I + 1_000_001)); + assert_eq!(exp_fixed_i(-1_000_000), Ok(SCALE_I - 1_000_000)); + + let mut previous = exp_fixed_i(-1_000_008).unwrap(); + for x in -1_000_007..=1_000_008 { + let current = exp_fixed_i(x).unwrap(); + assert!(current >= previous, "exp reversed at raw input {x}"); + previous = current; + } + } + #[test] fn pow_zero_to_zero_is_domain_error() { assert_eq!(pow_fixed(0, 0), Err(SolMathError::DomainError)); @@ -464,7 +595,10 @@ mod tests { #[test] fn pow_fixed_i_negative_fractional_is_domain_error() { // (-2)^0.5 is undefined in reals - assert_eq!(pow_fixed_i(-2 * SCALE_I, SCALE_I / 2), Err(SolMathError::DomainError)); + assert_eq!( + pow_fixed_i(-2 * SCALE_I, SCALE_I / 2), + Err(SolMathError::DomainError) + ); } // ── HP transcendental errors ── @@ -497,12 +631,18 @@ mod tests { #[test] fn pow_product_hp_w_exceeds_scale_is_domain_error() { - assert_eq!(pow_product_hp(SCALE, SCALE + 1), Err(SolMathError::DomainError)); + assert_eq!( + pow_product_hp(SCALE, SCALE + 1), + Err(SolMathError::DomainError) + ); } #[test] fn fp_div_hp_safe_division_by_zero() { - assert_eq!(fp_div_hp_safe(1_000_000_000_000_000, 0), Err(SolMathError::DivisionByZero)); + assert_eq!( + fp_div_hp_safe(1_000_000_000_000_000, 0), + Err(SolMathError::DivisionByZero) + ); } #[test] @@ -547,30 +687,41 @@ mod tests { // Φ⁻¹(0.25) = -Φ⁻¹(0.75) let z_lo = inverse_norm_cdf(250_000_000_000).unwrap(); let z_hi = inverse_norm_cdf(750_000_000_000).unwrap(); - assert!((z_lo + z_hi).abs() < 10, "asymmetry: {} + {} = {}", z_lo, z_hi, z_lo + z_hi); + assert!( + (z_lo + z_hi).abs() < 10, + "asymmetry: {} + {} = {}", + z_lo, + z_hi, + z_lo + z_hi + ); } #[test] fn inverse_norm_cdf_roundtrip() { // Φ(Φ⁻¹(p)) ≈ p for a range of probabilities let probs: &[i128] = &[ - 1_000_000_000, // 0.001 - 25_000_000_000, // 0.025 - 100_000_000_000, // 0.1 - 250_000_000_000, // 0.25 - 500_000_000_000, // 0.5 - 750_000_000_000, // 0.75 - 900_000_000_000, // 0.9 - 975_000_000_000, // 0.975 - 999_000_000_000, // 0.999 + 1_000_000_000, // 0.001 + 25_000_000_000, // 0.025 + 100_000_000_000, // 0.1 + 250_000_000_000, // 0.25 + 500_000_000_000, // 0.5 + 750_000_000_000, // 0.75 + 900_000_000_000, // 0.9 + 975_000_000_000, // 0.975 + 999_000_000_000, // 0.999 ]; for &p in probs { let z = inverse_norm_cdf(p).unwrap(); let p_back = norm_cdf_poly(z).unwrap(); let err = (p_back - p).abs(); - assert!(err <= 100, + assert!( + err <= 100, "roundtrip failed for p={}: z={}, Φ(z)={}, err={}", - p, z, p_back, err); + p, + z, + p_back, + err + ); } } @@ -579,8 +730,12 @@ mod tests { // Φ⁻¹(0.975) ≈ 1.95996 (z-score for 97.5th percentile) let z = inverse_norm_cdf(975_000_000_000).unwrap(); let expected = 1_959_963_984_540i128; // 1.95996398454 at SCALE - assert!((z - expected).abs() < 1000, - "Φ⁻¹(0.975) = {} (expected ~{})", z, expected); + assert!( + (z - expected).abs() < 1000, + "Φ⁻¹(0.975) = {} (expected ~{})", + z, + expected + ); } // ── Black-Scholes errors ── @@ -591,7 +746,10 @@ mod tests { let k = 100 * SCALE; let r = 50_000_000_000u128; let t = SCALE; - assert!(matches!(bs_full(s, k, r, 0, t), Err(SolMathError::DomainError))); + assert!(matches!( + bs_full(s, k, r, 0, t), + Err(SolMathError::DomainError) + )); } #[test] @@ -600,7 +758,10 @@ mod tests { let k = 100 * SCALE; let r = 50_000_000_000u128; let sigma = 200_000_000_000u128; - assert!(matches!(bs_full(s, k, r, sigma, 0), Err(SolMathError::DomainError))); + assert!(matches!( + bs_full(s, k, r, sigma, 0), + Err(SolMathError::DomainError) + )); } #[test] @@ -620,7 +781,10 @@ mod tests { let k = 100 * SCALE; let r = 50_000_000_000u128; let t = SCALE; - assert!(matches!(bs_full_hp(s, k, r, 0, t), Err(SolMathError::DomainError))); + assert!(matches!( + bs_full_hp(s, k, r, 0, t), + Err(SolMathError::DomainError) + )); } #[test] @@ -640,9 +804,27 @@ mod tests { #[test] fn black_scholes_price_hp_matches_full() { let cases = [ - (100 * SCALE, 105 * SCALE, 50_000_000_000u128, 250_000_000_000u128, SCALE), - (100 * SCALE, 90 * SCALE, 50_000_000_000u128, 300_000_000_000u128, SCALE / 4), - (100 * SCALE, 100 * SCALE, 50_000_000_000u128, 200_000_000_000u128, SCALE * 2), + ( + 100 * SCALE, + 105 * SCALE, + 50_000_000_000u128, + 250_000_000_000u128, + SCALE, + ), + ( + 100 * SCALE, + 90 * SCALE, + 50_000_000_000u128, + 300_000_000_000u128, + SCALE / 4, + ), + ( + 100 * SCALE, + 100 * SCALE, + 50_000_000_000u128, + 200_000_000_000u128, + SCALE * 2, + ), ]; for (s, k, r, sigma, t) in cases { @@ -676,18 +858,36 @@ mod tests { let s = 100 * SCALE; let k = 100 * SCALE; let r = 50_000_000_000u128; - assert!(matches!(black_scholes_price_hp(s, k, r, 0, SCALE), Err(SolMathError::DomainError))); - assert!(matches!(black_scholes_price_hp(s, k, r, 200_000_000_000, 0), Err(SolMathError::DomainError))); + assert!(matches!( + black_scholes_price_hp(s, k, r, 0, SCALE), + Err(SolMathError::DomainError) + )); + assert!(matches!( + black_scholes_price_hp(s, k, r, 200_000_000_000, 0), + Err(SolMathError::DomainError) + )); } // ── Implied volatility errors ── #[test] fn implied_vol_zero_inputs_is_domain_error() { - assert_eq!(implied_vol(0, SCALE, SCALE, 0, SCALE), Err(SolMathError::DomainError)); - assert_eq!(implied_vol(SCALE, 0, SCALE, 0, SCALE), Err(SolMathError::DomainError)); - assert_eq!(implied_vol(SCALE, SCALE, 0, 0, SCALE), Err(SolMathError::DomainError)); - assert_eq!(implied_vol(SCALE, SCALE, SCALE, 0, 0), Err(SolMathError::DomainError)); + assert_eq!( + implied_vol(0, SCALE, SCALE, 0, SCALE), + Err(SolMathError::DomainError) + ); + assert_eq!( + implied_vol(SCALE, 0, SCALE, 0, SCALE), + Err(SolMathError::DomainError) + ); + assert_eq!( + implied_vol(SCALE, SCALE, 0, 0, SCALE), + Err(SolMathError::DomainError) + ); + assert_eq!( + implied_vol(SCALE, SCALE, SCALE, 0, 0), + Err(SolMathError::DomainError) + ); } #[test] @@ -695,13 +895,23 @@ mod tests { // ATM: S=K=100, r=5%, σ=20%, T=1yr let s = 100 * SCALE; let k = 100 * SCALE; - let r = 50_000_000_000u128; // 0.05 + let r = 50_000_000_000u128; // 0.05 let sigma_in = 200_000_000_000u128; // 0.20 let t = SCALE; // 1.0 let bs = bs_full(s, k, r, sigma_in, t).unwrap(); let sigma_out = implied_vol(bs.call, s, k, r, t).unwrap(); - let err = if sigma_out > sigma_in { sigma_out - sigma_in } else { sigma_in - sigma_out }; - assert!(err <= 1000, "ATM IV roundtrip: in={} out={} err={}", sigma_in, sigma_out, err); + let err = if sigma_out > sigma_in { + sigma_out - sigma_in + } else { + sigma_in - sigma_out + }; + assert!( + err <= 1000, + "ATM IV roundtrip: in={} out={} err={}", + sigma_in, + sigma_out, + err + ); } #[test] @@ -714,8 +924,18 @@ mod tests { let t = SCALE / 2; let bs = bs_full(s, k, r, sigma_in, t).unwrap(); let sigma_out = implied_vol(bs.call, s, k, r, t).unwrap(); - let err = if sigma_out > sigma_in { sigma_out - sigma_in } else { sigma_in - sigma_out }; - assert!(err <= 1000, "OTM IV roundtrip: in={} out={} err={}", sigma_in, sigma_out, err); + let err = if sigma_out > sigma_in { + sigma_out - sigma_in + } else { + sigma_in - sigma_out + }; + assert!( + err <= 1000, + "OTM IV roundtrip: in={} out={} err={}", + sigma_in, + sigma_out, + err + ); } #[test] @@ -728,16 +948,37 @@ mod tests { let t = SCALE; let bs = bs_full(s, k, r, sigma_in, t).unwrap(); let sigma_out = implied_vol(bs.call, s, k, r, t).unwrap(); - let err = if sigma_out > sigma_in { sigma_out - sigma_in } else { sigma_in - sigma_out }; - assert!(err <= 1000, "ITM IV roundtrip: in={} out={} err={}", sigma_in, sigma_out, err); + let err = if sigma_out > sigma_in { + sigma_out - sigma_in + } else { + sigma_in - sigma_out + }; + assert!( + err <= 1000, + "ITM IV roundtrip: in={} out={} err={}", + sigma_in, + sigma_out, + err + ); } #[test] fn implied_vol_batch_roundtrip() { // Test a grid of scenarios let spots = [100 * SCALE]; - let strikes = [80 * SCALE, 90 * SCALE, 100 * SCALE, 110 * SCALE, 120 * SCALE]; - let sigmas = [100_000_000_000u128, 200_000_000_000, 500_000_000_000, 1_000_000_000_000]; + let strikes = [ + 80 * SCALE, + 90 * SCALE, + 100 * SCALE, + 110 * SCALE, + 120 * SCALE, + ]; + let sigmas = [ + 100_000_000_000u128, + 200_000_000_000, + 500_000_000_000, + 1_000_000_000_000, + ]; let times = [SCALE / 10, SCALE / 4, SCALE / 2, SCALE]; let rate = 50_000_000_000u128; @@ -751,9 +992,13 @@ mod tests { total += 1; let bs = match bs_full(s, k, rate, sigma_in, t) { Ok(v) => v, - Err(_) => { continue; } + Err(_) => { + continue; + } }; - if bs.call < 2 { continue; } + if bs.call < 2 { + continue; + } match implied_vol(bs.call, s, k, rate, t) { Ok(sigma_out) => { let err = if sigma_out > sigma_in { @@ -761,7 +1006,9 @@ mod tests { } else { sigma_in - sigma_out }; - if err <= 1000 { pass += 1; } + if err <= 1000 { + pass += 1; + } } Err(_) => {} } @@ -770,27 +1017,35 @@ mod tests { } } let pct = pass as f64 / total as f64 * 100.0; - assert!(pct >= 90.0, - "IV batch roundtrip: {}/{} passed ({:.1}%), need ≥90%", pass, total, pct); + assert!( + pct >= 90.0, + "IV batch roundtrip: {}/{} passed ({:.1}%), need ≥90%", + pass, + total, + pct + ); } // ── NIG errors ── #[test] fn nig_zero_inputs_is_domain_error() { - assert!(nig_call_price(0, SCALE, 0, SCALE, 10*SCALE, 0, SCALE).is_err()); - assert!(nig_call_price(SCALE, 0, 0, SCALE, 10*SCALE, 0, SCALE).is_err()); + assert!(nig_call_price(0, SCALE, 0, SCALE, 10 * SCALE, 0, SCALE).is_err()); + assert!(nig_call_price(SCALE, 0, 0, SCALE, 10 * SCALE, 0, SCALE).is_err()); } #[test] - fn nig_invalid_alpha_beta_is_domain_error() { - // alpha=1, beta=2 → α² < β², invalid + fn nig_invalid_shape_is_domain_error() { + // Invalid NIG shapes are rejected before numerical evaluation. let s = 100 * SCALE; let k = 100 * SCALE; let alpha = SCALE; let beta = 2 * SCALE_I; let delta = SCALE; - assert_eq!(nig_call_price(s, k, 0, SCALE, alpha, beta, delta), Err(SolMathError::DomainError)); + assert_eq!( + nig_call_price(s, k, 0, SCALE, alpha, beta, delta), + Err(SolMathError::DomainError) + ); } // ── Pool math errors ── @@ -829,7 +1084,10 @@ mod tests { fn complex_div_by_zero_is_error() { let a = Complex::new(SCALE_I, 0); let b = Complex::new(0, 0); - assert!(matches!(complex_div(a, b), Err(SolMathError::DivisionByZero))); + assert!(matches!( + complex_div(a, b), + Err(SolMathError::DivisionByZero) + )); } // ── Total function sanity checks ── @@ -841,21 +1099,9 @@ mod tests { #[test] fn trig_handles_extreme_i128_inputs() { - let sin_min = sin_fixed(i128::MIN).unwrap(); - let cos_min = cos_fixed(i128::MIN).unwrap(); - let (sin_pair, cos_pair) = sincos_fixed(i128::MIN).unwrap(); - assert_eq!(sin_min, sin_pair); - assert_eq!(cos_min, cos_pair); - assert!(sin_min.abs() <= SCALE_I); - assert!(cos_min.abs() <= SCALE_I); - - let sin_max = sin_fixed(i128::MAX).unwrap(); - let cos_max = cos_fixed(i128::MAX).unwrap(); - let (sin_pair_max, cos_pair_max) = sincos_fixed(i128::MAX).unwrap(); - assert_eq!(sin_max, sin_pair_max); - assert_eq!(cos_max, cos_pair_max); - assert!(sin_max.abs() <= SCALE_I); - assert!(cos_max.abs() <= SCALE_I); + assert_eq!(sin_fixed(i128::MIN), Err(SolMathError::DomainError)); + assert_eq!(cos_fixed(i128::MIN), Err(SolMathError::DomainError)); + assert_eq!(sincos_fixed(i128::MAX), Err(SolMathError::DomainError)); } #[test] @@ -895,48 +1141,87 @@ mod tests { // Group 1: Basic arithmetic #[test] - fn mul_div_floor_exact() { assert_eq!(mul_div_floor(10, 20, 5).unwrap(), 40); } + fn mul_div_floor_exact() { + assert_eq!(mul_div_floor(10, 20, 5).unwrap(), 40); + } #[test] - fn mul_div_ceil_exact() { assert_eq!(mul_div_ceil(10, 20, 5).unwrap(), 40); } + fn mul_div_ceil_exact() { + assert_eq!(mul_div_ceil(10, 20, 5).unwrap(), 40); + } #[test] - fn mul_div_floor_truncates() { assert_eq!(mul_div_floor(100, 200, 300).unwrap(), 66); } + fn mul_div_floor_truncates() { + assert_eq!(mul_div_floor(100, 200, 300).unwrap(), 66); + } #[test] - fn mul_div_ceil_rounds_up() { assert_eq!(mul_div_ceil(100, 200, 300).unwrap(), 67); } + fn mul_div_ceil_rounds_up() { + assert_eq!(mul_div_ceil(100, 200, 300).unwrap(), 67); + } #[test] - fn mul_div_floor_one_third() { assert_eq!(mul_div_floor(1, 1, 3).unwrap(), 0); } + fn mul_div_floor_one_third() { + assert_eq!(mul_div_floor(1, 1, 3).unwrap(), 0); + } #[test] - fn mul_div_ceil_one_third() { assert_eq!(mul_div_ceil(1, 1, 3).unwrap(), 1); } + fn mul_div_ceil_one_third() { + assert_eq!(mul_div_ceil(1, 1, 3).unwrap(), 1); + } #[test] - fn mul_div_floor_two_thirds() { assert_eq!(mul_div_floor(2, 1, 3).unwrap(), 0); } + fn mul_div_floor_two_thirds() { + assert_eq!(mul_div_floor(2, 1, 3).unwrap(), 0); + } #[test] - fn mul_div_ceil_two_thirds() { assert_eq!(mul_div_ceil(2, 1, 3).unwrap(), 1); } + fn mul_div_ceil_two_thirds() { + assert_eq!(mul_div_ceil(2, 1, 3).unwrap(), 1); + } #[test] - fn mul_div_floor_half() { assert_eq!(mul_div_floor(7, 3, 2).unwrap(), 10); } + fn mul_div_floor_half() { + assert_eq!(mul_div_floor(7, 3, 2).unwrap(), 10); + } #[test] - fn mul_div_ceil_half() { assert_eq!(mul_div_ceil(7, 3, 2).unwrap(), 11); } + fn mul_div_ceil_half() { + assert_eq!(mul_div_ceil(7, 3, 2).unwrap(), 11); + } // Group 2: Identity and zero #[test] - fn mul_div_floor_zero_a() { assert_eq!(mul_div_floor(0, 1000, 1).unwrap(), 0); } + fn mul_div_floor_zero_a() { + assert_eq!(mul_div_floor(0, 1000, 1).unwrap(), 0); + } #[test] - fn mul_div_ceil_zero_a() { assert_eq!(mul_div_ceil(0, 1000, 1).unwrap(), 0); } + fn mul_div_ceil_zero_a() { + assert_eq!(mul_div_ceil(0, 1000, 1).unwrap(), 0); + } #[test] - fn mul_div_floor_zero_b() { assert_eq!(mul_div_floor(1000, 0, 1).unwrap(), 0); } + fn mul_div_floor_zero_b() { + assert_eq!(mul_div_floor(1000, 0, 1).unwrap(), 0); + } #[test] - fn mul_div_ceil_zero_b() { assert_eq!(mul_div_ceil(1000, 0, 1).unwrap(), 0); } + fn mul_div_ceil_zero_b() { + assert_eq!(mul_div_ceil(1000, 0, 1).unwrap(), 0); + } #[test] - fn mul_div_floor_identity() { assert_eq!(mul_div_floor(1000, 1, 1).unwrap(), 1000); } + fn mul_div_floor_identity() { + assert_eq!(mul_div_floor(1000, 1, 1).unwrap(), 1000); + } #[test] - fn mul_div_ceil_identity() { assert_eq!(mul_div_ceil(1000, 1, 1).unwrap(), 1000); } + fn mul_div_ceil_identity() { + assert_eq!(mul_div_ceil(1000, 1, 1).unwrap(), 1000); + } #[test] - fn mul_div_floor_zero_zero() { assert_eq!(mul_div_floor(0, 0, 1).unwrap(), 0); } + fn mul_div_floor_zero_zero() { + assert_eq!(mul_div_floor(0, 0, 1).unwrap(), 0); + } #[test] - fn mul_div_ceil_zero_zero() { assert_eq!(mul_div_ceil(0, 0, 1).unwrap(), 0); } + fn mul_div_ceil_zero_zero() { + assert_eq!(mul_div_ceil(0, 0, 1).unwrap(), 0); + } // Group 3: Division by zero #[test] fn mul_div_floor_div_by_zero() { - assert_eq!(mul_div_floor(100, 200, 0), Err(SolMathError::DivisionByZero)); + assert_eq!( + mul_div_floor(100, 200, 0), + Err(SolMathError::DivisionByZero) + ); } #[test] fn mul_div_ceil_div_by_zero() { @@ -954,22 +1239,41 @@ mod tests { // Group 4: Large values that would overflow u64 without u128 #[test] fn mul_div_floor_large_cancelling() { - assert_eq!(mul_div_floor(10_u64.pow(18), 10_u64.pow(12), 10_u64.pow(12)).unwrap(), 10_u64.pow(18)); + assert_eq!( + mul_div_floor(10_u64.pow(18), 10_u64.pow(12), 10_u64.pow(12)).unwrap(), + 10_u64.pow(18) + ); } #[test] fn mul_div_ceil_large_cancelling() { - assert_eq!(mul_div_ceil(10_u64.pow(18), 10_u64.pow(12), 10_u64.pow(12)).unwrap(), 10_u64.pow(18)); + assert_eq!( + mul_div_ceil(10_u64.pow(18), 10_u64.pow(12), 10_u64.pow(12)).unwrap(), + 10_u64.pow(18) + ); } #[test] - fn mul_div_floor_max_identity() { assert_eq!(mul_div_floor(u64::MAX, 1, 1).unwrap(), u64::MAX); } + fn mul_div_floor_max_identity() { + assert_eq!(mul_div_floor(u64::MAX, 1, 1).unwrap(), u64::MAX); + } #[test] - fn mul_div_ceil_max_identity() { assert_eq!(mul_div_ceil(u64::MAX, 1, 1).unwrap(), u64::MAX); } + fn mul_div_ceil_max_identity() { + assert_eq!(mul_div_ceil(u64::MAX, 1, 1).unwrap(), u64::MAX); + } #[test] - fn mul_div_floor_max_times_2_div_2() { assert_eq!(mul_div_floor(u64::MAX, 2, 2).unwrap(), u64::MAX); } + fn mul_div_floor_max_times_2_div_2() { + assert_eq!(mul_div_floor(u64::MAX, 2, 2).unwrap(), u64::MAX); + } #[test] - fn mul_div_ceil_max_times_2_div_2() { assert_eq!(mul_div_ceil(u64::MAX, 2, 2).unwrap(), u64::MAX); } + fn mul_div_ceil_max_times_2_div_2() { + assert_eq!(mul_div_ceil(u64::MAX, 2, 2).unwrap(), u64::MAX); + } #[test] - fn mul_div_floor_max_cubed() { assert_eq!(mul_div_floor(u64::MAX, u64::MAX, u64::MAX).unwrap(), u64::MAX); } + fn mul_div_floor_max_cubed() { + assert_eq!( + mul_div_floor(u64::MAX, u64::MAX, u64::MAX).unwrap(), + u64::MAX + ); + } // Group 5: Overflow detection #[test] @@ -982,11 +1286,17 @@ mod tests { } #[test] fn mul_div_floor_overflow_max_sq() { - assert_eq!(mul_div_floor(u64::MAX, u64::MAX, 1), Err(SolMathError::Overflow)); + assert_eq!( + mul_div_floor(u64::MAX, u64::MAX, 1), + Err(SolMathError::Overflow) + ); } #[test] fn mul_div_ceil_overflow_max_sq() { - assert_eq!(mul_div_ceil(u64::MAX, u64::MAX, 1), Err(SolMathError::Overflow)); + assert_eq!( + mul_div_ceil(u64::MAX, u64::MAX, 1), + Err(SolMathError::Overflow) + ); } // Group 6: Folio-specific values @@ -994,14 +1304,22 @@ mod tests { fn mul_div_ceil_proportional_join() { // B_k=10^12, desired_lp=50*10^9, supply=100*10^9 assert_eq!( - mul_div_ceil(1_000_000_000_000_u64, 50_000_000_000_u64, 100_000_000_000_u64).unwrap(), + mul_div_ceil( + 1_000_000_000_000_u64, + 50_000_000_000_u64, + 100_000_000_000_u64 + ) + .unwrap(), 500_000_000_000 ); } #[test] fn mul_div_floor_ratio_cap() { // 30% of 10^12 balance - assert_eq!(mul_div_floor(1_000_000_000_000_u64, 30, 100).unwrap(), 300_000_000_000); + assert_eq!( + mul_div_floor(1_000_000_000_000_u64, 30, 100).unwrap(), + 300_000_000_000 + ); } #[test] fn mul_div_floor_fee_calc() { @@ -1024,8 +1342,14 @@ mod tests { #[test] fn mul_div_u128_div_by_zero() { - assert_eq!(mul_div_floor_u128(100, 200, 0), Err(SolMathError::DivisionByZero)); - assert_eq!(mul_div_ceil_u128(100, 200, 0), Err(SolMathError::DivisionByZero)); + assert_eq!( + mul_div_floor_u128(100, 200, 0), + Err(SolMathError::DivisionByZero) + ); + assert_eq!( + mul_div_ceil_u128(100, 200, 0), + Err(SolMathError::DivisionByZero) + ); } #[test] @@ -1057,21 +1381,39 @@ mod tests { #[test] fn mul_div_u128_max_cubed() { - assert_eq!(mul_div_floor_u128(u128::MAX, u128::MAX, u128::MAX).unwrap(), u128::MAX); + assert_eq!( + mul_div_floor_u128(u128::MAX, u128::MAX, u128::MAX).unwrap(), + u128::MAX + ); } #[test] fn mul_div_u128_overflow() { - assert_eq!(mul_div_floor_u128(u128::MAX, 2, 1), Err(SolMathError::Overflow)); - assert_eq!(mul_div_ceil_u128(u128::MAX, 2, 1), Err(SolMathError::Overflow)); - assert_eq!(mul_div_floor_u128(u128::MAX, u128::MAX, 1), Err(SolMathError::Overflow)); - assert_eq!(mul_div_ceil_u128(u128::MAX, u128::MAX, 1), Err(SolMathError::Overflow)); + assert_eq!( + mul_div_floor_u128(u128::MAX, 2, 1), + Err(SolMathError::Overflow) + ); + assert_eq!( + mul_div_ceil_u128(u128::MAX, 2, 1), + Err(SolMathError::Overflow) + ); + assert_eq!( + mul_div_floor_u128(u128::MAX, u128::MAX, 1), + Err(SolMathError::Overflow) + ); + assert_eq!( + mul_div_ceil_u128(u128::MAX, u128::MAX, 1), + Err(SolMathError::Overflow) + ); } #[test] fn mul_div_u128_scale_values() { // Typical SCALE-valued operations - assert_eq!(mul_div_floor_u128(100 * SCALE, 100 * SCALE, SCALE).unwrap(), 10000 * SCALE); + assert_eq!( + mul_div_floor_u128(100 * SCALE, 100 * SCALE, SCALE).unwrap(), + 10000 * SCALE + ); assert_eq!(mul_div_floor_u128(SCALE, SCALE, SCALE).unwrap(), SCALE); } @@ -1091,7 +1433,11 @@ mod tests { let in_ = barrier_option(s, k, h, r, sigma, t, true, BarrierType::DownAndIn).unwrap(); let sum = out.price + in_.price; - assert_eq!(sum, out.vanilla, "In/Out conservation: in={} out={} vanilla={}", in_.price, out.price, out.vanilla); + assert_eq!( + sum, out.vanilla, + "In/Out conservation: in={} out={} vanilla={}", + in_.price, out.price, out.vanilla + ); } #[test] @@ -1108,7 +1454,11 @@ mod tests { let in_ = barrier_option(s, k, h, r, sigma, t, false, BarrierType::UpAndIn).unwrap(); let sum = out.price + in_.price; - assert_eq!(sum, out.vanilla, "Put In/Out conservation: in={} out={} vanilla={}", in_.price, out.price, out.vanilla); + assert_eq!( + sum, out.vanilla, + "Put In/Out conservation: in={} out={} vanilla={}", + in_.price, out.price, out.vanilla + ); } #[test] @@ -1128,7 +1478,12 @@ mod tests { result.vanilla - result.price }; // Should be very close to vanilla (within 1%) - assert!(diff * 100 < result.vanilla, "Far barrier should ≈ vanilla: price={} vanilla={}", result.price, result.vanilla); + assert!( + diff * 100 < result.vanilla, + "Far barrier should ≈ vanilla: price={} vanilla={}", + result.price, + result.vanilla + ); } #[test] @@ -1170,8 +1525,16 @@ mod tests { let t = SCALE; let result = barrier_option(s, k, h, r, sigma, t, true, BarrierType::DownAndOut).unwrap(); - assert!(result.price <= result.vanilla, "Out should be ≤ vanilla: {} vs {}", result.price, result.vanilla); - assert!(result.price > 0, "Out should be positive when spot is above barrier"); + assert!( + result.price <= result.vanilla, + "Out should be ≤ vanilla: {} vs {}", + result.price, + result.vanilla + ); + assert!( + result.price > 0, + "Out should be positive when spot is above barrier" + ); } #[test] @@ -1229,7 +1592,11 @@ mod tests { let t = SCALE / 4; let result = barrier_option(s, k, h, r, sigma, t, true, BarrierType::UpAndOut).unwrap(); - assert!(result.price > 0, "Up-and-out call should be positive: {}", result.price); + assert!( + result.price > 0, + "Up-and-out call should be positive: {}", + result.price + ); assert!(result.price <= result.vanilla, "Up-and-out ≤ vanilla"); } @@ -1247,7 +1614,11 @@ mod tests { let in_ = barrier_option(s, k, h, r, sigma, t, true, BarrierType::UpAndIn).unwrap(); let sum = out.price + in_.price; - assert_eq!(sum, out.vanilla, "Up call In/Out conservation: in={} out={} vanilla={}", in_.price, out.price, out.vanilla); + assert_eq!( + sum, out.vanilla, + "Up call In/Out conservation: in={} out={} vanilla={}", + in_.price, out.price, out.vanilla + ); } #[test] @@ -1267,7 +1638,11 @@ mod tests { assert!(out.price > 0, "Down-out call K out.vanilla { sum - out.vanilla } else { out.vanilla - sum }; + let diff = if sum > out.vanilla { + sum - out.vanilla + } else { + out.vanilla - sum + }; assert!(diff < 100, "Down K out.vanilla { sum - out.vanilla } else { out.vanilla - sum }; + let diff = if sum > out.vanilla { + sum - out.vanilla + } else { + out.vanilla - sum + }; assert!(diff < 100, "Down put K>H conservation: diff={}", diff); } @@ -1305,7 +1684,11 @@ mod tests { assert!(out.price <= out.vanilla); let sum = out.price + in_.price; - let diff = if sum > out.vanilla { sum - out.vanilla } else { out.vanilla - sum }; + let diff = if sum > out.vanilla { + sum - out.vanilla + } else { + out.vanilla - sum + }; assert!(diff < 100, "Up put K 0, "Up-out put K>H should be positive: out={} in={} vanilla={}", out.price, in_.price, out.vanilla); - assert!(out.price <= out.vanilla, "Up-out ({}) > vanilla ({}), in={}", out.price, out.vanilla, in_.price); + assert!( + out.price > 0, + "Up-out put K>H should be positive: out={} in={} vanilla={}", + out.price, + in_.price, + out.vanilla + ); + assert!( + out.price <= out.vanilla, + "Up-out ({}) > vanilla ({}), in={}", + out.price, + out.vanilla, + in_.price + ); let sum = out.price + in_.price; - let diff = if sum > out.vanilla { sum - out.vanilla } else { out.vanilla - sum }; + let diff = if sum > out.vanilla { + sum - out.vanilla + } else { + out.vanilla - sum + }; assert!(diff < 100, "Up put K>H conservation: diff={}", diff); } } @@ -1352,7 +1751,11 @@ mod mul_div_properties { let product = (a as u128) * (b as u128); let c128 = c as u128; let floor = product / c128; - let ceil = if product % c128 == 0 { floor } else { floor + 1 }; + let ceil = if product % c128 == 0 { + floor + } else { + floor + 1 + }; if floor > u64::MAX as u128 || ceil > u64::MAX as u128 { return false; } @@ -1416,7 +1819,11 @@ mod mul_div_properties { checked += usize::from(check_case(a, b, c)); } - assert!(checked > 1_000, "checked too few non-overflow cases: {}", checked); + assert!( + checked > 1_000, + "checked too few non-overflow cases: {}", + checked + ); } } @@ -1459,8 +1866,14 @@ mod mul_div_u128_properties { && c <= u64::MAX as u128 && ceil <= u64::MAX as u128 { - assert_eq!(mul_div_floor(a as u64, b as u64, c as u64).unwrap() as u128, f); - assert_eq!(mul_div_ceil(a as u64, b as u64, c as u64).unwrap() as u128, ce); + assert_eq!( + mul_div_floor(a as u64, b as u64, c as u64).unwrap() as u128, + f + ); + assert_eq!( + mul_div_ceil(a as u64, b as u64, c as u64).unwrap() as u128, + ce + ); } true @@ -1511,7 +1924,11 @@ mod mul_div_u128_properties { checked += usize::from(check_case(a, b, c)); } - assert!(checked > 1_000, "checked too few non-overflow cases: {}", checked); + assert!( + checked > 1_000, + "checked too few non-overflow cases: {}", + checked + ); } } @@ -1531,14 +1948,62 @@ mod mul_div_cross_validation { #[test] fn validate_against_exact_vectors() { let vectors = [ - Vector { a: 0, b: 0, c: 1, floor: 0, ceil: 0 }, - Vector { a: 0, b: u64::MAX, c: 1, floor: 0, ceil: 0 }, - Vector { a: u64::MAX, b: 1, c: 1, floor: u64::MAX, ceil: u64::MAX }, - Vector { a: u64::MAX, b: 2, c: 2, floor: u64::MAX, ceil: u64::MAX }, - Vector { a: 100, b: 200, c: 300, floor: 66, ceil: 67 }, - Vector { a: 1, b: 1, c: 3, floor: 0, ceil: 1 }, - Vector { a: 2, b: 1, c: 3, floor: 0, ceil: 1 }, - Vector { a: 7, b: 3, c: 2, floor: 10, ceil: 11 }, + Vector { + a: 0, + b: 0, + c: 1, + floor: 0, + ceil: 0, + }, + Vector { + a: 0, + b: u64::MAX, + c: 1, + floor: 0, + ceil: 0, + }, + Vector { + a: u64::MAX, + b: 1, + c: 1, + floor: u64::MAX, + ceil: u64::MAX, + }, + Vector { + a: u64::MAX, + b: 2, + c: 2, + floor: u64::MAX, + ceil: u64::MAX, + }, + Vector { + a: 100, + b: 200, + c: 300, + floor: 66, + ceil: 67, + }, + Vector { + a: 1, + b: 1, + c: 3, + floor: 0, + ceil: 1, + }, + Vector { + a: 2, + b: 1, + c: 3, + floor: 0, + ceil: 1, + }, + Vector { + a: 7, + b: 3, + c: 2, + floor: 10, + ceil: 11, + }, Vector { a: 1_000_000_000_000, b: 50_000_000_000, @@ -1553,19 +2018,30 @@ mod mul_div_cross_validation { floor: 10_u64.pow(18), ceil: 10_u64.pow(18), }, - Vector { a: u64::MAX, b: u64::MAX, c: u64::MAX, floor: u64::MAX, ceil: u64::MAX }, + Vector { + a: u64::MAX, + b: u64::MAX, + c: u64::MAX, + floor: u64::MAX, + ceil: u64::MAX, + }, ]; for v in &vectors { let f = mul_div_floor(v.a, v.b, v.c).unwrap(); let ce = mul_div_ceil(v.a, v.b, v.c).unwrap(); if f != v.floor { - panic!("FLOOR mismatch: a={} b={} c={} expected={} got={}", v.a, v.b, v.c, v.floor, f); + panic!( + "FLOOR mismatch: a={} b={} c={} expected={} got={}", + v.a, v.b, v.c, v.floor, f + ); } if ce != v.ceil { - panic!("CEIL mismatch: a={} b={} c={} expected={} got={}", v.a, v.b, v.c, v.ceil, ce); + panic!( + "CEIL mismatch: a={} b={} c={} expected={} got={}", + v.a, v.b, v.c, v.ceil, ce + ); } } } - } diff --git a/src/ln2_lut.rs b/src/ln2_lut.rs new file mode 100644 index 0000000..ea81af6 --- /dev/null +++ b/src/ln2_lut.rs @@ -0,0 +1,161 @@ +// @generated by scripts/generate_ln2_lut.py; do not edit manually. +// Shared by ln_1p_fixed and expm1_fixed after power-of-two range reduction. + +pub(crate) const K_LN2_MIN: i32 = -64; +pub(crate) const K_LN2_MAX: i32 = 88; +pub(crate) const K_LN2_ENTRIES: usize = 153; +pub(crate) const K_LN2_RAW: [i64; K_LN2_ENTRIES] = [ + -44361419555836, + -43668272375277, + -42975125194717, + -42281978014157, + -41588830833597, + -40895683653037, + -40202536472477, + -39509389291917, + -38816242111357, + -38123094930797, + -37429947750237, + -36736800569677, + -36043653389117, + -35350506208557, + -34657359027997, + -33964211847437, + -33271064666877, + -32577917486317, + -31884770305757, + -31191623125198, + -30498475944638, + -29805328764078, + -29112181583518, + -28419034402958, + -27725887222398, + -27032740041838, + -26339592861278, + -25646445680718, + -24953298500158, + -24260151319598, + -23567004139038, + -22873856958478, + -22180709777918, + -21487562597358, + -20794415416798, + -20101268236238, + -19408121055678, + -18714973875119, + -18021826694559, + -17328679513999, + -16635532333439, + -15942385152879, + -15249237972319, + -14556090791759, + -13862943611199, + -13169796430639, + -12476649250079, + -11783502069519, + -11090354888959, + -10397207708399, + -9704060527839, + -9010913347279, + -8317766166719, + -7624618986159, + -6931471805599, + -6238324625040, + -5545177444480, + -4852030263920, + -4158883083360, + -3465735902800, + -2772588722240, + -2079441541680, + -1386294361120, + -693147180560, + 0, + 693147180560, + 1386294361120, + 2079441541680, + 2772588722240, + 3465735902800, + 4158883083360, + 4852030263920, + 5545177444480, + 6238324625040, + 6931471805599, + 7624618986159, + 8317766166719, + 9010913347279, + 9704060527839, + 10397207708399, + 11090354888959, + 11783502069519, + 12476649250079, + 13169796430639, + 13862943611199, + 14556090791759, + 15249237972319, + 15942385152879, + 16635532333439, + 17328679513999, + 18021826694559, + 18714973875119, + 19408121055678, + 20101268236238, + 20794415416798, + 21487562597358, + 22180709777918, + 22873856958478, + 23567004139038, + 24260151319598, + 24953298500158, + 25646445680718, + 26339592861278, + 27032740041838, + 27725887222398, + 28419034402958, + 29112181583518, + 29805328764078, + 30498475944638, + 31191623125198, + 31884770305757, + 32577917486317, + 33271064666877, + 33964211847437, + 34657359027997, + 35350506208557, + 36043653389117, + 36736800569677, + 37429947750237, + 38123094930797, + 38816242111357, + 39509389291917, + 40202536472477, + 40895683653037, + 41588830833597, + 42281978014157, + 42975125194717, + 43668272375277, + 44361419555836, + 45054566736396, + 45747713916956, + 46440861097516, + 47134008278076, + 47827155458636, + 48520302639196, + 49213449819756, + 49906597000316, + 50599744180876, + 51292891361436, + 51986038541996, + 52679185722556, + 53372332903116, + 54065480083676, + 54758627264236, + 55451774444796, + 56144921625356, + 56838068805916, + 57531215986475, + 58224363167035, + 58917510347595, + 59610657528155, + 60303804708715, + 60996951889275, +]; diff --git a/src/ln_lut.rs b/src/ln_lut.rs new file mode 100644 index 0000000..ff3137d --- /dev/null +++ b/src/ln_lut.rs @@ -0,0 +1,2060 @@ +// @generated by scripts/generate_ln_lut.py; do not edit manually. +// Midpoints partition normalized mantissas [1, 2) into 1024 equal bins. + +pub(crate) const LN_LUT_SEGMENTS: usize = 1024; +pub(crate) const LN_LUT_STEP: u128 = 976562500; +pub(crate) const LN_LUT_HALF_STEP: u128 = 488281250; + +pub(crate) const LN_LUT_MID_LOG: [i64; LN_LUT_SEGMENTS] = [ + 488162080, + 1463771913, + 2438430860, + 3412140771, + 4384903494, + 5356720868, + 6327594731, + 7297526912, + 8266519236, + 9234573522, + 10201691586, + 11167875236, + 12133126276, + 13097446505, + 14060837717, + 15023301699, + 15984840234, + 16945455102, + 17905148074, + 18863920918, + 19821775397, + 20778713269, + 21734736287, + 22689846197, + 23644044743, + 24597333661, + 25549714686, + 26501189543, + 27451759957, + 28401427645, + 29350194319, + 30298061689, + 31245031457, + 32191105321, + 33136284975, + 34080572109, + 35023968405, + 35966475544, + 36908095199, + 37848829041, + 38788678734, + 39727645939, + 40665732312, + 41602939503, + 42539269160, + 43474722923, + 44409302430, + 45343009314, + 46275845203, + 47207811719, + 48138910483, + 49069143108, + 49998511205, + 50927016378, + 51854660230, + 52781444355, + 53707370347, + 54632439794, + 55556654277, + 56480015377, + 57402524667, + 58324183719, + 59244994097, + 60164957363, + 61084075075, + 62002348785, + 62919780042, + 63836370391, + 64752121370, + 65667034517, + 66581111363, + 67494353436, + 68406762258, + 69318339350, + 70229086225, + 71139004395, + 72048095367, + 72956360643, + 73863801722, + 74770420097, + 75676217261, + 76581194698, + 77485353892, + 78388696320, + 79291223457, + 80192936773, + 81093837735, + 81993927805, + 82893208441, + 83791681098, + 84689347226, + 85586208273, + 86482265681, + 87377520888, + 88271975331, + 89165630439, + 90058487642, + 90950548361, + 91841814017, + 92732286026, + 93621965800, + 94510854747, + 95398954273, + 96286265777, + 97172790658, + 98058530309, + 98943486119, + 99827659474, + 100711051758, + 101593664348, + 102475498620, + 103356555946, + 104236837693, + 105116345226, + 105995079904, + 106873043086, + 107750236125, + 108626660370, + 109502317169, + 110377207863, + 111251333793, + 112124696294, + 112997296699, + 113869136335, + 114740216530, + 115610538604, + 116480103876, + 117348913661, + 118216969271, + 119084272014, + 119950823195, + 120816624115, + 121681676072, + 122545980361, + 123409538273, + 124272351096, + 125134420115, + 125995746611, + 126856331862, + 127716177143, + 128575283725, + 129433652876, + 130291285862, + 131148183944, + 132004348379, + 132859780425, + 133714481332, + 134568452348, + 135421694721, + 136274209692, + 137125998500, + 137977062381, + 138827402568, + 139677020291, + 140525916777, + 141374093248, + 142221550926, + 143068291028, + 143914314767, + 144759623355, + 145604217999, + 146448099906, + 147291270276, + 148133730309, + 148975481201, + 149816524144, + 150656860327, + 151496490939, + 152335417163, + 153173640179, + 154011161166, + 154847981299, + 155684101749, + 156519523686, + 157354248275, + 158188276680, + 159021610062, + 159854249577, + 160686196380, + 161517451624, + 162348016455, + 163177892022, + 164007079466, + 164835579927, + 165663394544, + 166490524451, + 167316970779, + 168142734657, + 168967817212, + 169792219567, + 170615942843, + 171438988157, + 172261356624, + 173083049357, + 173904067466, + 174724412057, + 175544084234, + 176363085098, + 177181415750, + 177999077283, + 178816070793, + 179632397369, + 180448058099, + 181263054069, + 182077386362, + 182891056057, + 183704064232, + 184516411961, + 185328100317, + 186139130370, + 186949503186, + 187759219829, + 188568281362, + 189376688843, + 190184443330, + 190991545876, + 191797997532, + 192603799349, + 193408952371, + 194213457644, + 195017316208, + 195820529103, + 196623097364, + 197425022026, + 198226304120, + 199026944676, + 199826944718, + 200626305272, + 201425027360, + 202223111999, + 203020560207, + 203817372998, + 204613551384, + 205409096374, + 206204008976, + 206998290193, + 207791941029, + 208584962482, + 209377355550, + 210169121229, + 210960260511, + 211750774386, + 212540663842, + 213329929866, + 214118573440, + 214906595545, + 215693997161, + 216480779263, + 217266942825, + 218052488820, + 218837418217, + 219621731982, + 220405431082, + 221188516478, + 221970989132, + 222752850000, + 223534100040, + 224314740205, + 225094771445, + 225874194712, + 226653010950, + 227431221106, + 228208826122, + 228985826938, + 229762224493, + 230538019722, + 231313213559, + 232087806936, + 232861800783, + 233635196027, + 234407993592, + 235180194403, + 235951799379, + 236722809441, + 237493225503, + 238263048482, + 239032279289, + 239800918835, + 240568968028, + 241336427774, + 242103298977, + 242869582540, + 243635279361, + 244400390340, + 245164916371, + 245928858348, + 246692217164, + 247454993707, + 248217188866, + 248978803526, + 249739838570, + 250500294881, + 251260173337, + 252019474816, + 252778200194, + 253536350344, + 254293926138, + 255050928445, + 255807358133, + 256563216068, + 257318503113, + 258073220130, + 258827367978, + 259580947517, + 260333959600, + 261086405084, + 261838284818, + 262589599655, + 263340350441, + 264090538023, + 264840163245, + 265589226951, + 266337729980, + 267085673171, + 267833057361, + 268579883385, + 269326152077, + 270071864266, + 270817020784, + 271561622456, + 272305670109, + 273049164568, + 273792106652, + 274534497184, + 275276336980, + 276017626859, + 276758367633, + 277498560117, + 278238205121, + 278977303455, + 279715855926, + 280453863339, + 281191326500, + 281928246209, + 282664623267, + 283400458473, + 284135752624, + 284870506514, + 285604720938, + 286338396686, + 287071534549, + 287804135314, + 288536199769, + 289267728697, + 289998722881, + 290729183104, + 291459110143, + 292188504778, + 292917367784, + 293645699936, + 294373502006, + 295100774765, + 295827518983, + 296553735428, + 297279424864, + 298004588058, + 298729225771, + 299453338764, + 300176927797, + 300899993628, + 301622537012, + 302344558704, + 303066059456, + 303787040021, + 304507501147, + 305227443583, + 305946868075, + 306665775366, + 307384166202, + 308102041323, + 308819401468, + 309536247377, + 310252579786, + 310968399430, + 311683707043, + 312398503356, + 313112789101, + 313826565005, + 314539831797, + 315252590202, + 315964840945, + 316676584747, + 317387822330, + 318098554414, + 318808781717, + 319518504955, + 320227724843, + 320936442094, + 321644657421, + 322352371534, + 323059585142, + 323766298953, + 324472513671, + 325178230002, + 325883448649, + 326588170314, + 327292395695, + 327996125492, + 328699360401, + 329402101119, + 330104348339, + 330806102753, + 331507365054, + 332208135931, + 332908416071, + 333608206163, + 334307506891, + 335006318939, + 335704642989, + 336402479724, + 337099829822, + 337796693962, + 338493072820, + 339188967072, + 339884377392, + 340579304453, + 341273748925, + 341967711479, + 342661192783, + 343354193504, + 344046714307, + 344738755857, + 345430318817, + 346121403848, + 346812011610, + 347502142762, + 348191797962, + 348880977865, + 349569683127, + 350257914399, + 350945672336, + 351632957586, + 352319770800, + 353006112625, + 353691983708, + 354377384695, + 355062316228, + 355746778952, + 356430773507, + 357114300532, + 357797360668, + 358479954551, + 359162082818, + 359843746103, + 360524945039, + 361205680260, + 361885952395, + 362565762074, + 363245109926, + 363923996578, + 364602422656, + 365280388784, + 365957895585, + 366634943681, + 367311533693, + 367987666241, + 368663341942, + 369338561414, + 370013325272, + 370687634131, + 371361488604, + 372034889303, + 372707836839, + 373380331820, + 374052374856, + 374723966554, + 375395107519, + 376065798355, + 376736039667, + 377405832057, + 378075176125, + 378744072471, + 379412521693, + 380080524390, + 380748081157, + 381415192590, + 382081859281, + 382748081824, + 383413860810, + 384079196829, + 384744090471, + 385408542322, + 386072552971, + 386736123002, + 387399253000, + 388061943547, + 388724195227, + 389386008620, + 390047384306, + 390708322862, + 391368824868, + 392028890898, + 392688521529, + 393347717334, + 394006478886, + 394664806757, + 395322701518, + 395980163738, + 396637193985, + 397293792826, + 397949960829, + 398605698557, + 399261006575, + 399915885446, + 400570335731, + 401224357992, + 401877952786, + 402531120673, + 403183862211, + 403836177955, + 404488068460, + 405139534281, + 405790575971, + 406441194081, + 407091389163, + 407741161765, + 408390512437, + 409039441727, + 409687950180, + 410336038343, + 410983706759, + 411630955973, + 412277786526, + 412924198959, + 413570193814, + 414215771628, + 414860932941, + 415505678289, + 416150008208, + 416793923234, + 417437423899, + 418080510738, + 418723184282, + 419365445062, + 420007293608, + 420648730448, + 421289756111, + 421930371124, + 422570576011, + 423210371298, + 423849757509, + 424488735167, + 425127304793, + 425765466908, + 426403222031, + 427040570683, + 427677513379, + 428314050638, + 428950182975, + 429585910905, + 430221234942, + 430856155598, + 431490673386, + 432124788816, + 432758502399, + 433391814643, + 434024726056, + 434657237146, + 435289348419, + 435921060379, + 436552373532, + 437183288379, + 437813805424, + 438443925167, + 439073648110, + 439702974751, + 440331905590, + 440960441123, + 441588581847, + 442216328258, + 442843680851, + 443470640119, + 444097206555, + 444723380652, + 445349162900, + 445974553790, + 446599553810, + 447224163450, + 447848383195, + 448472213534, + 449095654950, + 449718707930, + 450341372956, + 450963650512, + 451585541080, + 452207045139, + 452828163172, + 453448895656, + 454069243070, + 454689205893, + 455308784599, + 455927979665, + 456546791566, + 457165220776, + 457783267767, + 458400933012, + 459018216982, + 459635120148, + 460251642979, + 460867785943, + 461483549510, + 462098934145, + 462713940314, + 463328568483, + 463942819117, + 464556692678, + 465170189630, + 465783310434, + 466396055551, + 467008425442, + 467620420565, + 468232041379, + 468843288342, + 469454161910, + 470064662539, + 470674790685, + 471284546802, + 471893931342, + 472502944759, + 473111587505, + 473719860030, + 474327762784, + 474935296217, + 475542460777, + 476149256912, + 476755685069, + 477361745694, + 477967439231, + 478572766126, + 479177726822, + 479782321762, + 480386551388, + 480990416140, + 481593916461, + 482197052788, + 482799825561, + 483402235217, + 484004282195, + 484605966931, + 485207289859, + 485808251416, + 486408852034, + 487009092148, + 487608972190, + 488208492592, + 488807653784, + 489406456197, + 490004900261, + 490602986403, + 491200715052, + 491798086635, + 492395101578, + 492991760307, + 493588063246, + 494184010821, + 494779603453, + 495374841566, + 495969725581, + 496564255919, + 497158433002, + 497752257247, + 498345729075, + 498938848902, + 499531617147, + 500124034226, + 500716100555, + 501307816548, + 501899182621, + 502490199187, + 503080866658, + 503671185448, + 504261155966, + 504850778625, + 505440053833, + 506028982001, + 506617563537, + 507205798848, + 507793688342, + 508381232424, + 508968431502, + 509555285979, + 510141796259, + 510727962747, + 511313785845, + 511899265956, + 512484403480, + 513069198818, + 513653652371, + 514237764537, + 514821535715, + 515404966304, + 515988056699, + 516570807298, + 517153218497, + 517735290690, + 518317024273, + 518898419638, + 519479477179, + 520060197288, + 520640580357, + 521220626777, + 521800336939, + 522379711231, + 522958750042, + 523537453762, + 524115822778, + 524693857476, + 525271558243, + 525848925465, + 526425959526, + 527002660810, + 527579029702, + 528155066585, + 528730771839, + 529306145848, + 529881188993, + 530455901652, + 531030284207, + 531604337035, + 532178060516, + 532751455027, + 533324520946, + 533897258647, + 534469668509, + 535041750904, + 535613506208, + 536184934795, + 536756037038, + 537326813309, + 537897263981, + 538467389424, + 539037190009, + 539606666106, + 540175818085, + 540744646315, + 541313151162, + 541881332996, + 542449192182, + 543016729087, + 543583944077, + 544150837516, + 544717409769, + 545283661200, + 545849592171, + 546415203046, + 546980494186, + 547545465953, + 548110118706, + 548674452807, + 549238468614, + 549802166487, + 550365546784, + 550928609862, + 551491356078, + 552053785789, + 552615899351, + 553177697118, + 553739179446, + 554300346689, + 554861199199, + 555421737331, + 555981961435, + 556541871864, + 557101468969, + 557660753100, + 558219724607, + 558778383839, + 559336731146, + 559894766874, + 560452491373, + 561009904988, + 561567008067, + 562123800954, + 562680283996, + 563236457536, + 563792321920, + 564347877489, + 564903124589, + 565458063560, + 566012694744, + 566567018483, + 567121035118, + 567674744988, + 568228148433, + 568781245793, + 569334037404, + 569886523606, + 570438704736, + 570990581129, + 571542153124, + 572093421054, + 572644385255, + 573195046062, + 573745403808, + 574295458828, + 574845211453, + 575394662016, + 575943810849, + 576492658283, + 577041204649, + 577589450277, + 578137395496, + 578685040636, + 579232386024, + 579779431990, + 580326178860, + 580872626961, + 581418776619, + 581964628161, + 582510181912, + 583055438196, + 583600397337, + 584145059660, + 584689425488, + 585233495142, + 585777268945, + 586320747220, + 586863930286, + 587406818464, + 587949412075, + 588491711437, + 589033716871, + 589575428693, + 590116847223, + 590657972777, + 591198805673, + 591739346227, + 592279594754, + 592819551571, + 593359216991, + 593898591330, + 594437674901, + 594976468018, + 595514970992, + 596053184138, + 596591107766, + 597128742187, + 597666087713, + 598203144654, + 598739913320, + 599276394019, + 599812587062, + 600348492755, + 600884111408, + 601419443326, + 601954488818, + 602489248189, + 603023721745, + 603557909792, + 604091812634, + 604625430577, + 605158763922, + 605691812976, + 606224578039, + 606757059415, + 607289257405, + 607821172311, + 608352804434, + 608884154075, + 609415221533, + 609946007108, + 610476511099, + 611006733805, + 611536675524, + 612066336553, + 612595717190, + 613124817732, + 613653638474, + 614182179712, + 614710441742, + 615238424859, + 615766129357, + 616293555530, + 616820703670, + 617347574072, + 617874167028, + 618400482830, + 618926521769, + 619452284136, + 619977770223, + 620502980319, + 621027914714, + 621552573698, + 622076957559, + 622601066586, + 623124901066, + 623648461288, + 624171747537, + 624694760101, + 625217499266, + 625739965318, + 626262158541, + 626784079221, + 627305727642, + 627827104088, + 628348208841, + 628869042187, + 629389604405, + 629909895780, + 630429916593, + 630949667124, + 631469147656, + 631988358467, + 632507299838, + 633025972049, + 633544375378, + 634062510105, + 634580376507, + 635097974862, + 635615305448, + 636132368541, + 636649164418, + 637165693355, + 637681955628, + 638197951512, + 638713681281, + 639229145210, + 639744343572, + 640259276642, + 640773944693, + 641288347996, + 641802486825, + 642316361451, + 642829972145, + 643343319179, + 643856402823, + 644369223347, + 644881781021, + 645394076114, + 645906108896, + 646417879634, + 646929388596, + 647440636051, + 647951622266, + 648462347506, + 648972812040, + 649483016133, + 649992960050, + 650502644057, + 651012068419, + 651521233399, + 652030139262, + 652538786272, + 653047174692, + 653555304784, + 654063176812, + 654570791036, + 655078147718, + 655585247121, + 656092089503, + 656598675127, + 657105004252, + 657611077137, + 658116894042, + 658622455226, + 659127760947, + 659632811463, + 660137607031, + 660642147910, + 661146434355, + 661650466624, + 662154244972, + 662657769655, + 663161040929, + 663664059048, + 664166824268, + 664669336841, + 665171597022, + 665673605065, + 666175361222, + 666676865746, + 667178118889, + 667679120904, + 668179872041, + 668680372552, + 669180622687, + 669680622698, + 670180372833, + 670679873343, + 671179124477, + 671678126483, + 672176879611, + 672675384108, + 673173640221, + 673671648200, + 674169408290, + 674666920738, + 675164185790, + 675661203693, + 676157974692, + 676654499032, + 677150776958, + 677646808714, + 678142594545, + 678638134694, + 679133429404, + 679628478919, + 680123283481, + 680617843333, + 681112158716, + 681606229873, + 682100057043, + 682593640469, + 683086980390, + 683580077047, + 684072930679, + 684565541527, + 685057909828, + 685550035823, + 686041919748, + 686533561842, + 687024962344, + 687516121489, + 688007039516, + 688497716660, + 688988153158, + 689478349246, + 689968305160, + 690458021135, + 690947497405, + 691436734205, + 691925731770, + 692414490333, + 692903010128, +]; + +pub(crate) const LN_Q42_RECIP_G32: [i64; LN_LUT_SEGMENTS] = [ + 18880247061, + 18861836288, + 18843461387, + 18825122252, + 18806818779, + 18788550863, + 18770318403, + 18752121293, + 18733959432, + 18715832718, + 18697741048, + 18679684320, + 18661662435, + 18643675290, + 18625722787, + 18607804823, + 18589921301, + 18572072121, + 18554257184, + 18536476391, + 18518729645, + 18501016847, + 18483337901, + 18465692710, + 18448081177, + 18430503205, + 18412958700, + 18395447564, + 18377969704, + 18360525025, + 18343113432, + 18325734831, + 18308389128, + 18291076231, + 18273796045, + 18256548479, + 18239333441, + 18222150837, + 18205000578, + 18187882571, + 18170796725, + 18153742951, + 18136721157, + 18119731254, + 18102773153, + 18085846764, + 18068951998, + 18052088767, + 18035256983, + 18018456557, + 18001687402, + 17984949432, + 17968242558, + 17951566695, + 17934921756, + 17918307655, + 17901724307, + 17885171626, + 17868649528, + 17852157927, + 17835696739, + 17819265881, + 17802865268, + 17786494817, + 17770154445, + 17753844070, + 17737563607, + 17721312976, + 17705092095, + 17688900881, + 17672739254, + 17656607133, + 17640504436, + 17624431083, + 17608386995, + 17592372091, + 17576386292, + 17560429518, + 17544501691, + 17528602731, + 17512732561, + 17496891103, + 17481078277, + 17465294008, + 17449538217, + 17433810828, + 17418111764, + 17402440948, + 17386798305, + 17371183757, + 17355597231, + 17340038650, + 17324507939, + 17309005024, + 17293529829, + 17278082281, + 17262662306, + 17247269830, + 17231904778, + 17216567080, + 17201256660, + 17185973446, + 17170717367, + 17155488349, + 17140286322, + 17125111212, + 17109962949, + 17094841462, + 17079746679, + 17064678530, + 17049636945, + 17034621853, + 17019633184, + 17004670869, + 16989734839, + 16974825023, + 16959941354, + 16945083762, + 16930252178, + 16915446536, + 16900666766, + 16885912801, + 16871184574, + 16856482016, + 16841805062, + 16827153644, + 16812527696, + 16797927151, + 16783351943, + 16768802006, + 16754277275, + 16739777684, + 16725303168, + 16710853662, + 16696429101, + 16682029421, + 16667654557, + 16653304446, + 16638979023, + 16624678224, + 16610401987, + 16596150248, + 16581922944, + 16567720012, + 16553541390, + 16539387015, + 16525256825, + 16511150759, + 16497068754, + 16483010749, + 16468976683, + 16454966494, + 16440980122, + 16427017506, + 16413078586, + 16399163301, + 16385271592, + 16371403397, + 16357558659, + 16343737316, + 16329939311, + 16316164584, + 16302413075, + 16288684727, + 16274979482, + 16261297279, + 16247638063, + 16234001774, + 16220388355, + 16206797749, + 16193229899, + 16179684746, + 16166162235, + 16152662308, + 16139184909, + 16125729982, + 16112297471, + 16098887319, + 16085499471, + 16072133871, + 16058790464, + 16045469194, + 16032170007, + 16018892848, + 16005637661, + 15992404393, + 15979192990, + 15966003396, + 15952835558, + 15939689422, + 15926564935, + 15913462043, + 15900380694, + 15887320833, + 15874282408, + 15861265366, + 15848269655, + 15835295222, + 15822342015, + 15809409983, + 15796499072, + 15783609232, + 15770740411, + 15757892557, + 15745065620, + 15732259548, + 15719474290, + 15706709796, + 15693966015, + 15681242897, + 15668540392, + 15655858449, + 15643197019, + 15630556052, + 15617935498, + 15605335308, + 15592755432, + 15580195823, + 15567656430, + 15555137205, + 15542638099, + 15530159064, + 15517700051, + 15505261013, + 15492841901, + 15480442668, + 15468063266, + 15455703647, + 15443363764, + 15431043569, + 15418743016, + 15406462058, + 15394200648, + 15381958739, + 15369736284, + 15357533238, + 15345349555, + 15333185187, + 15321040090, + 15308914218, + 15296807524, + 15284719964, + 15272651491, + 15260602062, + 15248571631, + 15236560153, + 15224567583, + 15212593876, + 15200638989, + 15188702877, + 15176785495, + 15164886800, + 15153006748, + 15141145295, + 15129302396, + 15117478010, + 15105672092, + 15093884599, + 15082115488, + 15070364717, + 15058632241, + 15046918019, + 15035222008, + 15023544166, + 15011884450, + 15000242818, + 14988619228, + 14977013638, + 14965426007, + 14953856292, + 14942304453, + 14930770447, + 14919254234, + 14907755772, + 14896275020, + 14884811938, + 14873366485, + 14861938620, + 14850528302, + 14839135492, + 14827760149, + 14816402232, + 14805061702, + 14793738519, + 14782432643, + 14771144035, + 14759872655, + 14748618463, + 14737381420, + 14726161488, + 14714958626, + 14703772797, + 14692603960, + 14681452079, + 14670317113, + 14659199025, + 14648097776, + 14637013329, + 14625945644, + 14614894684, + 14603860411, + 14592842787, + 14581841774, + 14570857336, + 14559889435, + 14548938032, + 14538003092, + 14527084577, + 14516182449, + 14505296673, + 14494427212, + 14483574028, + 14472737085, + 14461916347, + 14451111777, + 14440323340, + 14429550999, + 14418794718, + 14408054461, + 14397330193, + 14386621877, + 14375929479, + 14365252962, + 14354592292, + 14343947433, + 14333318350, + 14322705008, + 14312107372, + 14301525408, + 14290959079, + 14280408353, + 14269873194, + 14259353567, + 14248849439, + 14238360776, + 14227887542, + 14217429705, + 14206987230, + 14196560084, + 14186148232, + 14175751641, + 14165370277, + 14155004108, + 14144653100, + 14134317219, + 14123996432, + 14113690707, + 14103400010, + 14093124309, + 14082863570, + 14072617762, + 14062386851, + 14052170806, + 14041969593, + 14031783180, + 14021611536, + 14011454628, + 14001312424, + 13991184892, + 13981072001, + 13970973719, + 13960890014, + 13950820854, + 13940766208, + 13930726045, + 13920700334, + 13910689043, + 13900692141, + 13890709597, + 13880741381, + 13870787461, + 13860847806, + 13850922387, + 13841011173, + 13831114132, + 13821231235, + 13811362452, + 13801507752, + 13791667104, + 13781840480, + 13772027849, + 13762229181, + 13752444446, + 13742673616, + 13732916659, + 13723173547, + 13713444249, + 13703728738, + 13694026983, + 13684338956, + 13674664626, + 13665003966, + 13655356946, + 13645723537, + 13636103711, + 13626497438, + 13616904691, + 13607325441, + 13597759658, + 13588207316, + 13578668385, + 13569142837, + 13559630644, + 13550131779, + 13540646212, + 13531173917, + 13521714865, + 13512269028, + 13502836380, + 13493416891, + 13484010536, + 13474617286, + 13465237114, + 13455869992, + 13446515894, + 13437174793, + 13427846660, + 13418531470, + 13409229195, + 13399939809, + 13390663284, + 13381399594, + 13372148713, + 13362910614, + 13353685270, + 13344472655, + 13335272743, + 13326085507, + 13316910922, + 13307748960, + 13298599597, + 13289462806, + 13280338561, + 13271226836, + 13262127606, + 13253040845, + 13243966528, + 13234904628, + 13225855121, + 13216817980, + 13207793181, + 13198780699, + 13189780507, + 13180792582, + 13171816897, + 13162853429, + 13153902152, + 13144963040, + 13136036071, + 13127121217, + 13118218456, + 13109327763, + 13100449112, + 13091582480, + 13082727842, + 13073885173, + 13065054450, + 13056235649, + 13047428745, + 13038633713, + 13029850531, + 13021079175, + 13012319619, + 13003571841, + 12994835817, + 12986111523, + 12977398936, + 12968698031, + 12960008786, + 12951331178, + 12942665182, + 12934010775, + 12925367934, + 12916736637, + 12908116859, + 12899508579, + 12890911772, + 12882326416, + 12873752488, + 12865189966, + 12856638826, + 12848099046, + 12839570603, + 12831053475, + 12822547639, + 12814053073, + 12805569754, + 12797097660, + 12788636769, + 12780187059, + 12771748507, + 12763321091, + 12754904790, + 12746499581, + 12738105442, + 12729722352, + 12721350289, + 12712989230, + 12704639155, + 12696300042, + 12687971869, + 12679654614, + 12671348257, + 12663052775, + 12654768148, + 12646494354, + 12638231371, + 12629979180, + 12621737758, + 12613507084, + 12605287138, + 12597077899, + 12588879345, + 12580691456, + 12572514211, + 12564347589, + 12556191570, + 12548046133, + 12539911257, + 12531786922, + 12523673107, + 12515569792, + 12507476957, + 12499394581, + 12491322644, + 12483261125, + 12475210006, + 12467169264, + 12459138882, + 12451118837, + 12443109112, + 12435109684, + 12427120536, + 12419141646, + 12411172996, + 12403214565, + 12395266334, + 12387328283, + 12379400393, + 12371482644, + 12363575017, + 12355677492, + 12347790050, + 12339912672, + 12332045339, + 12324188030, + 12316340728, + 12308503413, + 12300676066, + 12292858668, + 12285051200, + 12277253643, + 12269465978, + 12261688186, + 12253920249, + 12246162149, + 12238413865, + 12230675380, + 12222946675, + 12215227732, + 12207518532, + 12199819056, + 12192129287, + 12184449206, + 12176778794, + 12169118033, + 12161466906, + 12153825394, + 12146193478, + 12138571141, + 12130958366, + 12123355132, + 12115761424, + 12108177223, + 12100602511, + 12093037270, + 12085481483, + 12077935132, + 12070398199, + 12062870667, + 12055352517, + 12047843733, + 12040344297, + 12032854192, + 12025373400, + 12017901904, + 12010439686, + 12002986729, + 11995543016, + 11988108530, + 11980683254, + 11973267170, + 11965860262, + 11958462512, + 11951073904, + 11943694420, + 11936324044, + 11928962759, + 11921610548, + 11914267394, + 11906933280, + 11899608191, + 11892292108, + 11884985016, + 11877686898, + 11870397738, + 11863117518, + 11855846224, + 11848583837, + 11841330342, + 11834085723, + 11826849963, + 11819623045, + 11812404955, + 11805195675, + 11797995190, + 11790803483, + 11783620538, + 11776446340, + 11769280872, + 11762124119, + 11754976064, + 11747836692, + 11740705987, + 11733583933, + 11726470515, + 11719365716, + 11712269521, + 11705181915, + 11698102881, + 11691032405, + 11683970470, + 11676917062, + 11669872165, + 11662835764, + 11655807842, + 11648788385, + 11641777378, + 11634774805, + 11627780652, + 11620794902, + 11613817541, + 11606848553, + 11599887924, + 11592935639, + 11585991682, + 11579056039, + 11572128695, + 11565209635, + 11558298843, + 11551396306, + 11544502008, + 11537615934, + 11530738071, + 11523868403, + 11517006915, + 11510153593, + 11503308423, + 11496471390, + 11489642479, + 11482821676, + 11476008967, + 11469204337, + 11462407771, + 11455619256, + 11448838777, + 11442066320, + 11435301870, + 11428545414, + 11421796938, + 11415056426, + 11408323865, + 11401599242, + 11394882541, + 11388173750, + 11381472853, + 11374779838, + 11368094689, + 11361417394, + 11354747939, + 11348086309, + 11341432491, + 11334786472, + 11328148237, + 11321517772, + 11314895065, + 11308280102, + 11301672868, + 11295073351, + 11288481537, + 11281897413, + 11275320964, + 11268752178, + 11262191042, + 11255637541, + 11249091663, + 11242553394, + 11236022721, + 11229499631, + 11222984110, + 11216476146, + 11209975725, + 11203482835, + 11196997461, + 11190519591, + 11184049213, + 11177586313, + 11171130877, + 11164682894, + 11158242350, + 11151809233, + 11145383529, + 11138965225, + 11132554310, + 11126150770, + 11119754593, + 11113365765, + 11106984274, + 11100610108, + 11094243254, + 11087883700, + 11081531432, + 11075186438, + 11068848706, + 11062518224, + 11056194978, + 11049878957, + 11043570148, + 11037268539, + 11030974117, + 11024686870, + 11018406787, + 11012133854, + 11005868059, + 10999609391, + 10993357837, + 10987113385, + 10980876023, + 10974645738, + 10968422520, + 10962206355, + 10955997232, + 10949795139, + 10943600064, + 10937411995, + 10931230921, + 10925056828, + 10918889706, + 10912729542, + 10906576326, + 10900430044, + 10894290686, + 10888158240, + 10882032694, + 10875914036, + 10869802256, + 10863697340, + 10857599278, + 10851508058, + 10845423669, + 10839346099, + 10833275337, + 10827211371, + 10821154190, + 10815103782, + 10809060136, + 10803023241, + 10796993086, + 10790969659, + 10784952949, + 10778942944, + 10772939635, + 10766943008, + 10760953054, + 10754969760, + 10748993117, + 10743023112, + 10737059736, + 10731102976, + 10725152822, + 10719209262, + 10713272287, + 10707341884, + 10701418044, + 10695500754, + 10689590005, + 10683685785, + 10677788084, + 10671896890, + 10666012194, + 10660133984, + 10654262249, + 10648396980, + 10642538164, + 10636685793, + 10630839854, + 10625000337, + 10619167232, + 10613340529, + 10607520216, + 10601706283, + 10595898720, + 10590097516, + 10584302661, + 10578514145, + 10572731956, + 10566956085, + 10561186521, + 10555423254, + 10549666274, + 10543915570, + 10538171133, + 10532432951, + 10526701014, + 10520975313, + 10515255838, + 10509542577, + 10503835522, + 10498134662, + 10492439986, + 10486751485, + 10481069149, + 10475392967, + 10469722930, + 10464059028, + 10458401251, + 10452749589, + 10447104031, + 10441464569, + 10435831192, + 10430203890, + 10424582654, + 10418967473, + 10413358339, + 10407755240, + 10402158168, + 10396567113, + 10390982065, + 10385403014, + 10379829951, + 10374262866, + 10368701750, + 10363146592, + 10357597384, + 10352054115, + 10346516777, + 10340985359, + 10335459852, + 10329940248, + 10324426535, + 10318918706, + 10313416750, + 10307920658, + 10302430420, + 10296946028, + 10291467472, + 10285994743, + 10280527831, + 10275066727, + 10269611422, + 10264161907, + 10258718172, + 10253280209, + 10247848007, + 10242421559, + 10237000854, + 10231585884, + 10226176640, + 10220773112, + 10215375291, + 10209983169, + 10204596736, + 10199215984, + 10193840903, + 10188471485, + 10183107720, + 10177749599, + 10172397115, + 10167050257, + 10161709017, + 10156373386, + 10151043355, + 10145718916, + 10140400060, + 10135086777, + 10129779059, + 10124476898, + 10119180285, + 10113889210, + 10108603665, + 10103323643, + 10098049133, + 10092780127, + 10087516617, + 10082258595, + 10077006050, + 10071758976, + 10066517363, + 10061281204, + 10056050488, + 10050825209, + 10045605356, + 10040390923, + 10035181901, + 10029978280, + 10024780054, + 10019587213, + 10014399748, + 10009217653, + 10004040917, + 9998869534, + 9993703495, + 9988542791, + 9983387414, + 9978237356, + 9973092608, + 9967953164, + 9962819013, + 9957690149, + 9952566562, + 9947448246, + 9942335191, + 9937227390, + 9932124834, + 9927027515, + 9921935426, + 9916848559, + 9911766904, + 9906690455, + 9901619203, + 9896553141, + 9891492260, + 9886436552, + 9881386010, + 9876340625, + 9871300390, + 9866265297, + 9861235337, + 9856210504, + 9851190789, + 9846176184, + 9841166682, + 9836162275, + 9831162955, + 9826168714, + 9821179545, + 9816195440, + 9811216390, + 9806242390, + 9801273430, + 9796309503, + 9791350602, + 9786396718, + 9781447845, + 9776503975, + 9771565099, + 9766631211, + 9761702303, + 9756778368, + 9751859397, + 9746945384, + 9742036320, + 9737132199, + 9732233013, + 9727338755, + 9722449416, + 9717564991, + 9712685470, + 9707810848, + 9702941116, + 9698076267, + 9693216294, + 9688361189, + 9683510946, + 9678665556, + 9673825013, + 9668989310, + 9664158438, + 9659332391, + 9654511162, + 9649694744, + 9644883128, + 9640076309, + 9635274278, + 9630477030, + 9625684555, + 9620896848, + 9616113902, + 9611335709, + 9606562262, + 9601793554, + 9597029578, + 9592270327, + 9587515794, + 9582765972, + 9578020854, + 9573280432, + 9568544701, + 9563813653, + 9559087281, + 9554365579, + 9549648538, + 9544936153, + 9540228416, + 9535525321, + 9530826861, + 9526133028, + 9521443817, + 9516759220, + 9512079230, + 9507403841, + 9502733045, + 9498066837, + 9493405209, + 9488748155, + 9484095667, + 9479447740, + 9474804366, + 9470165539, + 9465531252, + 9460901499, + 9456276272, + 9451655565, + 9447039372, +]; diff --git a/src/lut_budget.rs b/src/lut_budget.rs new file mode 100644 index 0000000..c3e6d96 --- /dev/null +++ b/src/lut_budget.rs @@ -0,0 +1,61 @@ +//! Compile-time payload budgets for the reduced-domain transcendental tables. +//! +//! These limits cover raw table data. Linked SBF deltas also include the local +//! polynomial kernels and must be measured in the footprint harness before a +//! release changes either table. + +const _: () = { + const I64_BYTES: usize = core::mem::size_of::(); + const EXPM1_KERNEL_BYTES: usize = crate::expm1_lut::EXPM1_LUT_SEGMENTS * I64_BYTES; + const LN_1P_KERNEL_BYTES: usize = 2 * crate::ln_lut::LN_LUT_SEGMENTS * I64_BYTES; + const SHARED_LN2_BYTES: usize = crate::ln2_lut::K_LN2_ENTRIES * I64_BYTES; + const NORM_CDF_COEFFICIENT_BYTES: usize = + (7 * 9 + 3 * 8 + 4 * 7) * I64_BYTES + core::mem::size_of::(); + const EXP_COEFFICIENT_BYTES: usize = (crate::exp_coeffs::EXP_REMEZ_Q22.len() + + crate::exp_coeffs::EXP2_PHASE_Q62.len()) + * I64_BYTES; + + assert!( + crate::ln2_lut::K_LN2_ENTRIES + == (crate::ln2_lut::K_LN2_MAX - crate::ln2_lut::K_LN2_MIN + 1) as usize + ); + assert!(EXPM1_KERNEL_BYTES + SHARED_LN2_BYTES <= 16 * 1024); + assert!(LN_1P_KERNEL_BYTES + SHARED_LN2_BYTES <= 20 * 1024); + assert!(EXPM1_KERNEL_BYTES + LN_1P_KERNEL_BYTES + SHARED_LN2_BYTES <= 32 * 1024); + assert!(NORM_CDF_COEFFICIENT_BYTES <= 2 * 1024); + assert!(EXP_COEFFICIENT_BYTES <= 512); +}; + +#[cfg(test)] +mod tests { + #[test] + fn current_payload_accounting_is_explicit() { + let expm1_bytes = core::mem::size_of_val(&crate::expm1_lut::EXPM1_MID_EXP_RAW_Q22); + let ln1p_bytes = core::mem::size_of_val(&crate::ln_lut::LN_LUT_MID_LOG) + + core::mem::size_of_val(&crate::ln_lut::LN_Q42_RECIP_G32); + let shared_bytes = core::mem::size_of_val(&crate::ln2_lut::K_LN2_RAW); + let norm_cdf_bytes = core::mem::size_of_val(&crate::norm_cdf_coeffs::NORM_CDF_0_05_Q23) + + core::mem::size_of_val(&crate::norm_cdf_coeffs::NORM_CDF_05_10_Q23) + + core::mem::size_of_val(&crate::norm_cdf_coeffs::NORM_CDF_10_15_Q23) + + core::mem::size_of_val(&crate::norm_cdf_coeffs::NORM_CDF_15_20_Q23) + + core::mem::size_of_val(&crate::norm_cdf_coeffs::NORM_CDF_20_25_Q23) + + core::mem::size_of_val(&crate::norm_cdf_coeffs::NORM_CDF_25_30_Q23) + + core::mem::size_of_val(&crate::norm_cdf_coeffs::NORM_CDF_30_35_Q23) + + core::mem::size_of_val(&crate::norm_cdf_coeffs::NORM_CDF_35_40_Q23) + + core::mem::size_of_val(&crate::norm_cdf_coeffs::NORM_CDF_40_45_Q23) + + core::mem::size_of_val(&crate::norm_cdf_coeffs::NORM_CDF_45_50_Q23) + + core::mem::size_of_val(&crate::norm_cdf_coeffs::NORM_TAIL_50_55_Q23) + + core::mem::size_of_val(&crate::norm_cdf_coeffs::NORM_TAIL_55_60_Q23) + + core::mem::size_of_val(&crate::norm_cdf_coeffs::NORM_TAIL_60_65_Q23) + + core::mem::size_of_val(&crate::norm_cdf_coeffs::NORM_TAIL_65_70_Q23) + + core::mem::size_of_val(&crate::norm_cdf_coeffs::NORM_TAIL_HALF_RAW_CUTOFF); + let exp_bytes = core::mem::size_of_val(&crate::exp_coeffs::EXP_REMEZ_Q22) + + core::mem::size_of_val(&crate::exp_coeffs::EXP2_PHASE_Q62); + + assert_eq!(expm1_bytes + shared_bytes, 11_560); + assert_eq!(ln1p_bytes + shared_bytes, 17_608); + assert_eq!(expm1_bytes + ln1p_bytes + shared_bytes, 27_944); + assert_eq!(norm_cdf_bytes, 936); + assert_eq!(exp_bytes, 304); + } +} diff --git a/src/nig.rs b/src/nig.rs index 231711d..1a36e80 100644 --- a/src/nig.rs +++ b/src/nig.rs @@ -1,47 +1,665 @@ -use crate::constants::*; +//! Certified-domain European options in the exponential NIG Levy model. +//! +//! The implementation is entirely on-chain. It evaluates the smaller +//! out-of-the-money leg directly and obtains the other leg from put-call +//! parity. The remaining half-line integral is mapped to `[0, 1]` and +//! evaluated by an embedded 15/7 Gauss-Kronrod rule. A scaled `K1` kernel +//! keeps every density evaluation bounded, and a Chernoff bound handles deep +//! tails without entering the quadrature at all. +//! +//! This is intentionally not the former fixed-term COS implementation. There +//! is no uploaded surface, live oracle, lookup table, or trusted builder. + +use crate::arithmetic::{fp_div, fp_div_i, fp_mul, fp_mul_i, fp_sqrt}; +use crate::constants::{PI_SCALE, SCALE, SCALE_I}; use crate::error::SolMathError; -use crate::arithmetic::{fp_mul_i, fp_div_i, fp_sqrt}; -use crate::transcendental::{ln_fixed_i, exp_fixed_i}; -use crate::trig::{cos_fixed, sin_fixed}; -use crate::complex::{Complex, complex_mul, complex_sqrt, complex_exp}; - -/// NIG characteristic function φ(u). Internal — called by nig_call_price COS loop. -pub(crate) fn nig_char_func( - u: i128, - drift: i128, - delta_t: i128, +use crate::transcendental::{exp_fixed_i, expm1_fixed, ln_fixed_i}; + +/// Smallest supported NIG tail parameter, `alpha = 2`. +pub const NIG_MIN_ALPHA: u128 = 2 * SCALE; +/// Largest supported NIG tail parameter, `alpha = 100`. +pub const NIG_MAX_ALPHA: u128 = 100 * SCALE; +/// Largest supported elapsed NIG scale, `delta_per_year * time = 15`. +pub const NIG_MAX_DELTA_TIME: u128 = 15 * SCALE; +/// Smallest supported elapsed NIG scale, `1e-3`. +pub const NIG_MIN_DELTA_TIME: u128 = 1_000_000_000; +/// Number of nodes in the production Gauss-Kronrod rule. +pub const NIG_QUADRATURE_NODES: usize = 15; + +const NIG_MAX_PRICE: u128 = 100_000 * SCALE; +const NIG_MAX_TIME: u128 = 5 * SCALE; +const NIG_MAX_ABS_RATE: u128 = SCALE / 4; +const NIG_MAX_ABS_LOG_FORWARD: u128 = 2 * SCALE; +// 1e-5 of discounted notional: $0.001 on a $100 quote. This covers the +// fixed-point and embedded-rule residual inside the declared production +// domain; the embedded estimate may increase it quote by quote. +const NIG_ERROR_FLOOR_REL: u128 = 10_000_000; +// Compatibility APIs request at most 5e-5 of notional: $0.005 per $100. +const NIG_DEFAULT_ERROR_REL: u128 = 50_000_000; +const NIG_ROUNDING_REL: u128 = 1_000; // 1e-9 of notional +const NIG_QUADRATURE_SAFETY: u128 = 4; + +/// NIG Levy-process parameters, all at [`SCALE`]. +/// +/// `alpha` and `beta` are inverse log-return units. `delta_per_year` scales +/// linearly with time. The executable production domain additionally requires +/// both `|beta| / alpha <= 0.65` and `|beta + 1| / alpha <= 0.65`; the second +/// condition provides stock-numeraire moment headroom. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct NigParams { + pub alpha: u128, + pub beta: i128, + pub delta_per_year: u128, +} + +/// A call/put pair together with its quote-local absolute-error allowance. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CertifiedNigPrice { + pub call: u128, + pub put: u128, + pub max_abs_error: u128, + /// `0` is exact expiry, `1` is the Chernoff tail path, and `15` is the + /// embedded 15/7 Gauss-Kronrod path. + pub tier: u8, +} + +// Kronrod 15 / Gauss 7 geometry after the rational half-line map +// y = scale * t/(1-t). Constants are parameter-independent quadrature +// geometry, not sampled option values. +const GK_U: [u128; NIG_QUADRATURE_NODES] = [ + 4_290_645_426, + 26_110_451_522, + 72_464_022_021, + 148_414_691_932, + 260_964_690_514, + 422_631_787_036, + 655_923_922_307, + 1_000_000_000_000, + 1_524_567_051_134, + 2_366_125_858_665, + 3_831_936_029_467, + 6_737_877_409_459, + 13_799_951_646_520, + 38_298_839_801_385, + 233_065_168_689_945, +]; + +const GK_JAC: [u128; NIG_QUADRATURE_NODES] = [ + 1_008_599_700_490, + 1_052_902_658_724, + 1_150_179_078_529, + 1_318_856_304_645, + 1_590_031_950_724, + 2_023_881_201_485, + 2_742_084_036_468, + 4_000_000_000_000, + 6_373_438_795_673, + 11_330_803_296_376, + 23_347_605_792_862, + 59_874_746_803_810, + 219_038_568_739_327, + 1_544_398_809_734_951, + 54_786_503_193_852_536, +]; + +const GK_WEIGHT: [u128; NIG_QUADRATURE_NODES] = [ + 11_467_661_005, + 31_546_046_315, + 52_395_005_161, + 70_326_629_858, + 84_502_363_320, + 95_175_289_032, + 102_216_470_038, + 104_741_070_542, + 102_216_470_038, + 95_175_289_032, + 84_502_363_320, + 70_326_629_858, + 52_395_005_161, + 31_546_046_315, + 11_467_661_005, +]; + +const G7_WEIGHT: [u128; NIG_QUADRATURE_NODES] = [ + 0, + 64_742_483_084, + 0, + 139_852_695_745, + 0, + 190_915_025_253, + 0, + 208_979_591_837, + 0, + 190_915_025_253, + 0, + 139_852_695_745, + 0, + 64_742_483_084, + 0, +]; + +// Piecewise Chebyshev projections of H(x) = x exp(x) K1(x) on dyadic +// subintervals of [0, 1]. Coefficients are in ascending powers of the local +// coordinate. Dyadic coordinates avoid a fixed-point division at every node. +const K1_SMALL_H: [[i128; 7]; 9] = [ + [ + 1_001_941_935_150, + 1_932_638_831, + -8_385_258, + 563_767, + -89_961, + 145_057, + -108_873, + ], + [ + 1_005_777_191_439, + 1_903_908_444, + -6_364_790, + 198_229, + -17_346, + 2_473, + -424, + ], + [ + 1_011_435_965_003, + 3_739_596_495, + -20_613_099, + 757_583, + -68_299, + 9_810, + -1_685, + ], + [ + 1_022_485_681_062, + 7_262_743_550, + -64_164_215, + 2_816_927, + -265_263, + 38_607, + -6_667, + ], + [ + 1_043_756_979_814, + 13_868_979_760, + -189_918_073, + 10_056_339, + -1_005_742, + 149_694, + -26_096, + ], + [ + 1_083_867_776_206, + 25_856_671_251, + -527_679_967, + 33_879_718, + -3_664_028, + 564_981, + -100_141, + ], + [ + 1_157_392_995_973, + 46_688_295_973, + -1_356_781_762, + 105_515_666, + -12_545_612, + 2_036_542, + -370_796, + ], + [ + 1_287_386_842_132, + 81_038_835_579, + -3_184_255_272, + 297_187_115, + -39_292_940, + 6_826_578, + -1_292_872, + ], + [ + 1_507_696_399_816, + 134_561_132_879, + -6_753_634_018, + 742_551_509, + -109_483_434, + 20_617_628, + -4_110_566, + ], +]; + +// R(v) = sqrt(x) exp(x) K1(x), v = 1/x, x >= 1. +const K1_LARGE_R: [i128; 9] = [ + 1_253_314_240_630, + 469_974_369_594, + -146_312_355_748, + 121_580_534_756, + -133_560_147_736, + 135_711_020_822, + -100_936_922_398, + 45_468_297_214, + -9_085_608_557, +]; + +#[inline] +fn horner_ascending(x: i128, coefficients: &[i128]) -> Result { + let mut coefficients = coefficients.iter().rev(); + let mut value = *coefficients.next().ok_or(SolMathError::DomainError)?; + for coefficient in coefficients { + value = fp_mul_i(value, x)? + .checked_add(*coefficient) + .ok_or(SolMathError::Overflow)?; + } + Ok(value) +} + +/// Scaled modified Bessel function `exp(x) * K1(x)` at SCALE. +fn bessel_k1_scaled(x: u128) -> Result { + if x == 0 || x > i128::MAX as u128 { + return Err(SolMathError::DomainError); + } + + if x <= SCALE { + let (segment, multiplier, offset) = if x <= SCALE / 256 { + (0usize, 512u128, SCALE_I) + } else if x <= SCALE / 128 { + (1, 512, 3 * SCALE_I) + } else if x <= SCALE / 64 { + (2, 256, 3 * SCALE_I) + } else if x <= SCALE / 32 { + (3, 128, 3 * SCALE_I) + } else if x <= SCALE / 16 { + (4, 64, 3 * SCALE_I) + } else if x <= SCALE / 8 { + (5, 32, 3 * SCALE_I) + } else if x <= SCALE / 4 { + (6, 16, 3 * SCALE_I) + } else if x <= SCALE / 2 { + (7, 8, 3 * SCALE_I) + } else { + (8, 4, 3 * SCALE_I) + }; + let local = x + .checked_mul(multiplier) + .and_then(|value| (value as i128).checked_sub(offset)) + .ok_or(SolMathError::Overflow)?; + let h = horner_ascending(local, &K1_SMALL_H[segment])?; + if h <= 0 { + return Err(SolMathError::NoConvergence); + } + fp_div(h as u128, x) + } else { + let reciprocal = fp_div(SCALE, x)? as i128; + let ratio = horner_ascending(reciprocal, &K1_LARGE_R)?; + if ratio <= 0 { + return Err(SolMathError::NoConvergence); + } + fp_div(ratio as u128, fp_sqrt(x)?) + } +} + +#[inline] +fn hypot_fixed(delta: u128, x: i128) -> Result { + let x_abs = x.unsigned_abs(); + let squared = fp_mul(delta, delta)? + .checked_add(fp_mul(x_abs, x_abs)?) + .ok_or(SolMathError::Overflow)?; + fp_sqrt(squared) +} + +#[inline] +fn rounding_allowance(notional: u128) -> Result { + fp_mul(notional, NIG_ROUNDING_REL)? + .checked_add(4_096) + .ok_or(SolMathError::Overflow) +} + +fn chernoff_lower_tail( + x: i128, + beta: i128, gamma: i128, - alpha_sq: i128, + alpha: i128, + delta_t: i128, +) -> Result, SolMathError> { + let mean = fp_div_i(fp_mul_i(delta_t, beta)?, gamma)?; + if x >= mean { + return Ok(None); + } + // The optimized Chernoff exponent is + // d*gamma + beta*x - alpha*hypot(d,x) <= 0. + let omega = hypot_fixed(delta_t as u128, x)?; + let alpha_omega = fp_mul_i(alpha, omega as i128)?; + let exponent = fp_mul_i(delta_t, gamma)? + .checked_add(fp_mul_i(beta, x)?) + .and_then(|v| v.checked_sub(alpha_omega)) + .ok_or(SolMathError::Overflow)?; + // Fixed-point square-root rounding can lift the exact non-positive value + // by a few raw units at the mean. Clamping it to zero remains an upper + // bound. + Ok(Some(exp_fixed_i(exponent.min(0))? as u128)) +} + +struct IntegralInputs { + alpha: u128, beta: i128, -) -> Result { - // α² − (β+iu)² = (α²−β²+u², −2βu) - let u_sq = fp_mul_i(u, u)?; - let beta_sq = fp_mul_i(beta, beta)?; - let inner = complex_sqrt(Complex::new( - alpha_sq - beta_sq + u_sq, - -2 * fp_mul_i(beta, u)?, - ))?; - - // Exponent: iu·drift + δT·(γ − inner) - let exponent = Complex::new( - fp_mul_i(delta_t, gamma - inner.re)?, - fp_mul_i(u, drift)? - fp_mul_i(delta_t, inner.im)?, - ); - complex_exp(exponent) + delta_t: u128, + gamma: i128, + threshold: i128, + scale: u128, + call: bool, } -/// Offline/high-precision NIG call price via COS method (17 terms, i128 arithmetic). -/// ~302K CU native, exceeds on-chain budget with Anchor overhead. -/// For on-chain use, see `nig_call_64`. -/// -/// # Errors -/// - `DomainError` if s/k/alpha/delta == 0 or α ≤ |β| or α ≤ |β+1|. +fn payoff_density_node(inputs: &IntegralInputs, node: usize) -> Result { + let y = fp_mul(inputs.scale, GK_U[node])?; + if y > i128::MAX as u128 { + return Err(SolMathError::Overflow); + } + let y_i = y as i128; + let x = if inputs.call { + inputs.threshold.checked_add(y_i) + } else { + inputs.threshold.checked_sub(y_i) + } + .ok_or(SolMathError::Overflow)?; + let omega = hypot_fixed(inputs.delta_t, x)?; + let alpha_omega = fp_mul_i(inputs.alpha as i128, omega as i128)?; + let exponent = fp_mul_i(inputs.delta_t as i128, inputs.gamma)? + .checked_add(fp_mul_i(inputs.beta, x)?) + .and_then(|v| v.checked_sub(alpha_omega)) + .ok_or(SolMathError::Overflow)?; + + // Evaluate the payoff times the density as a difference of bounded + // exponentials. For small y, expm1 preserves the zero at the exercise + // boundary instead of subtracting two nearly equal values. + let exp_e = exp_fixed_i(exponent)?; + if exp_e < 0 { + return Err(SolMathError::NoConvergence); + } + let payoff_weight = if inputs.call { + if y_i < 20 * SCALE_I { + let growth = expm1_fixed(y_i)?; + if growth < 0 { + return Err(SolMathError::NoConvergence); + } + fp_mul(exp_e as u128, growth as u128)? + } else { + let grown = exp_fixed_i(exponent.checked_add(y_i).ok_or(SolMathError::Overflow)?)?; + grown + .checked_sub(exp_e) + .ok_or(SolMathError::NoConvergence)? as u128 + } + } else { + let decay = expm1_fixed(y_i.checked_neg().ok_or(SolMathError::Overflow)?)? + .checked_neg() + .ok_or(SolMathError::Overflow)?; + fp_mul(exp_e as u128, decay as u128)? + }; + + if payoff_weight == 0 { + return Ok(0); + } + let z = fp_mul(inputs.alpha, omega)?; + let scaled_k1 = bessel_k1_scaled(z)?; + let alpha_delta = fp_mul(inputs.alpha, inputs.delta_t)?; + let pi_omega = fp_mul(PI_SCALE as u128, omega)?; + let density_without_exp = fp_mul(fp_div(alpha_delta, pi_omega)?, scaled_k1)?; + let jacobian = fp_mul(inputs.scale, GK_JAC[node])?; + fp_mul(fp_mul(density_without_exp, payoff_weight)?, jacobian) +} + +fn integrate_otm(inputs: &IntegralInputs) -> Result<(u128, u128), SolMathError> { + let mut kronrod = 0u128; + let mut gauss = 0u128; + + let mut node = 0usize; + while node < NIG_QUADRATURE_NODES { + let value = payoff_density_node(inputs, node)?; + kronrod = kronrod + .checked_add(fp_mul(value, GK_WEIGHT[node])?) + .ok_or(SolMathError::Overflow)?; + if G7_WEIGHT[node] != 0 { + gauss = gauss + .checked_add(fp_mul(value, G7_WEIGHT[node])?) + .ok_or(SolMathError::Overflow)?; + } + node += 1; + } + Ok((kronrod, kronrod.abs_diff(gauss))) +} + +fn parity_prices( + otm: u128, + call_is_otm: bool, + discounted_spot: u128, + discounted_strike: u128, +) -> Result<(u128, u128), SolMathError> { + if call_is_otm { + let parity = discounted_strike + .checked_sub(discounted_spot) + .ok_or(SolMathError::NoConvergence)?; + Ok((otm, otm.checked_add(parity).ok_or(SolMathError::Overflow)?)) + } else { + let parity = discounted_spot + .checked_sub(discounted_strike) + .ok_or(SolMathError::NoConvergence)?; + Ok((otm.checked_add(parity).ok_or(SolMathError::Overflow)?, otm)) + } +} + +/// Price a European call and put under an exponential NIG Levy process. /// -/// # Precision -/// 95% within 0.5% of reference prices for α ≥ 10, prices > $1. +/// Rates and dividend yield are continuously compounded signed values. All +/// numeric fields and outputs use [`SCALE`]. The function fails closed when a +/// quote is outside the declared production domain or when its quote-local +/// embedded error allowance exceeds `requested_max_abs_error`. +pub fn nig_price_certified( + spot: u128, + strike: u128, + rate: i128, + dividend_yield: i128, + time: u128, + params: NigParams, + requested_max_abs_error: u128, +) -> Result { + if spot == 0 || strike == 0 || spot > NIG_MAX_PRICE || strike > NIG_MAX_PRICE { + return Err(SolMathError::DomainError); + } + if time == 0 { + return Ok(CertifiedNigPrice { + call: spot.saturating_sub(strike), + put: strike.saturating_sub(spot), + max_abs_error: 0, + tier: 0, + }); + } + if requested_max_abs_error == 0 + || time > NIG_MAX_TIME + || rate.unsigned_abs() > NIG_MAX_ABS_RATE + || dividend_yield.unsigned_abs() > NIG_MAX_ABS_RATE + || params.alpha < NIG_MIN_ALPHA + || params.alpha > NIG_MAX_ALPHA + || params.delta_per_year == 0 + || params.delta_per_year > 15 * SCALE + { + return Err(SolMathError::DomainError); + } + let beta_plus_one = params + .beta + .checked_add(SCALE_I) + .ok_or(SolMathError::Overflow)?; + let skew_limit = params.alpha.checked_mul(13).ok_or(SolMathError::Overflow)?; + if params + .beta + .unsigned_abs() + .checked_mul(20) + .ok_or(SolMathError::Overflow)? + > skew_limit + || beta_plus_one + .unsigned_abs() + .checked_mul(20) + .ok_or(SolMathError::Overflow)? + > skew_limit + { + return Err(SolMathError::DomainError); + } + + let delta_t = fp_mul(params.delta_per_year, time)?; + if !(NIG_MIN_DELTA_TIME..=NIG_MAX_DELTA_TIME).contains(&delta_t) { + return Err(SolMathError::DomainError); + } + let alpha_i = params.alpha as i128; + let alpha_sq = fp_mul(params.alpha, params.alpha)?; + let beta_sq = fp_mul_i(params.beta, params.beta)?; + let beta_one_sq = fp_mul_i(beta_plus_one, beta_plus_one)?; + if beta_sq < 0 + || beta_one_sq < 0 + || alpha_sq <= beta_sq as u128 + || alpha_sq <= beta_one_sq as u128 + { + return Err(SolMathError::DomainError); + } + let gamma = fp_sqrt(alpha_sq - beta_sq as u128)? as i128; + let gamma_one = fp_sqrt(alpha_sq - beta_one_sq as u128)? as i128; + + let log_moneyness = ln_fixed_i(spot)? + .checked_sub(ln_fixed_i(strike)?) + .ok_or(SolMathError::Overflow)?; + let carry = rate + .checked_sub(dividend_yield) + .ok_or(SolMathError::Overflow)?; + let carry_time = fp_mul_i(carry, time as i128)?; + let log_forward = log_moneyness + .checked_add(carry_time) + .ok_or(SolMathError::Overflow)?; + if log_forward.unsigned_abs() > NIG_MAX_ABS_LOG_FORWARD { + return Err(SolMathError::DomainError); + } + let correction = fp_mul_i( + delta_t as i128, + gamma_one.checked_sub(gamma).ok_or(SolMathError::Overflow)?, + )?; + let kappa = log_forward + .checked_add(correction) + .ok_or(SolMathError::Overflow)?; + let threshold = kappa.checked_neg().ok_or(SolMathError::Overflow)?; + + let rate_time = fp_mul_i(rate, time as i128)?; + let dividend_time = fp_mul_i(dividend_yield, time as i128)?; + let strike_discount = exp_fixed_i(rate_time.checked_neg().ok_or(SolMathError::Overflow)?)?; + let spot_discount = exp_fixed_i(dividend_time.checked_neg().ok_or(SolMathError::Overflow)?)?; + if strike_discount < 0 || spot_discount < 0 { + return Err(SolMathError::NoConvergence); + } + let discounted_strike = fp_mul(strike, strike_discount as u128)?; + let discounted_spot = fp_mul(spot, spot_discount as u128)?; + let call_is_otm = discounted_spot <= discounted_strike; + let notional = discounted_spot.max(discounted_strike); + let rounding = rounding_allowance(notional)?; + // A loose caller tolerance must not silently downgrade an otherwise + // computable quote to a zero-OTM tail approximation. The shortcut itself + // is capped at the crate's standard $0.005-per-$100 quality target. + let tail_accuracy_cap = fp_mul(notional, NIG_DEFAULT_ERROR_REL)?; + + // A rigorous zero-price shortcut: the OTM payoff is bounded by its asset + // (call) or cash (put) digital, then by the optimized NIG Chernoff tail. + let tail_probability = if call_is_otm { + chernoff_lower_tail( + kappa, + beta_plus_one.checked_neg().ok_or(SolMathError::Overflow)?, + gamma_one, + alpha_i, + delta_t as i128, + )? + } else { + chernoff_lower_tail(threshold, params.beta, gamma, alpha_i, delta_t as i128)? + }; + if let Some(probability) = tail_probability { + let digital_notional = if call_is_otm { + discounted_spot + } else { + discounted_strike + }; + let tail_bound = fp_mul(digital_notional, probability)?; + let certificate = tail_bound + .checked_add(rounding) + .ok_or(SolMathError::Overflow)?; + if certificate <= requested_max_abs_error && certificate <= tail_accuracy_cap { + let (call, put) = parity_prices(0, call_is_otm, discounted_spot, discounted_strike)?; + return Ok(CertifiedNigPrice { + call, + put, + max_abs_error: certificate, + tier: 1, + }); + } + } + + let gamma_sq = fp_mul(gamma as u128, gamma as u128)?; + let gamma_cube = fp_mul(gamma_sq, gamma as u128)?; + let variance = fp_div(fp_mul(delta_t, alpha_sq)?, gamma_cube)?; + let base_scale = fp_sqrt(variance)?; + let tilted_scale = if call_is_otm { + let gamma_one_sq = fp_mul(gamma_one as u128, gamma_one as u128)?; + let gamma_one_cube = fp_mul(gamma_one_sq, gamma_one as u128)?; + fp_sqrt(fp_div(fp_mul(delta_t, alpha_sq)?, gamma_one_cube)?)? + } else { + base_scale + }; + // A factor of four places the final Kronrod node far enough into the NIG + // tail for the 15/7 embedded pair while retaining dense central coverage. + let scale = base_scale + .max(tilted_scale) + .checked_mul(4) + .ok_or(SolMathError::Overflow)?; + if scale == 0 { + return Err(SolMathError::NoConvergence); + } + let integral_inputs = IntegralInputs { + alpha: params.alpha, + beta: params.beta, + delta_t, + gamma, + threshold, + scale, + call: call_is_otm, + }; + let (integral, integral_error) = integrate_otm(&integral_inputs)?; + let otm = fp_mul(discounted_strike, integral)?; + let (call, put) = parity_prices(otm, call_is_otm, discounted_spot, discounted_strike)?; + + let embedded_price_error = fp_mul(discounted_strike, integral_error)? + .checked_mul(NIG_QUADRATURE_SAFETY) + .ok_or(SolMathError::Overflow)?; + let floor = fp_mul(notional, NIG_ERROR_FLOOR_REL)?; + let certificate = floor.max( + embedded_price_error + .checked_add(rounding) + .ok_or(SolMathError::Overflow)?, + ); + if certificate > requested_max_abs_error { + return Err(SolMathError::NoConvergence); + } + if call + > discounted_spot + .checked_add(certificate) + .ok_or(SolMathError::Overflow)? + || put + > discounted_strike + .checked_add(certificate) + .ok_or(SolMathError::Overflow)? + { + return Err(SolMathError::NoConvergence); + } + + Ok(CertifiedNigPrice { + call, + put, + max_abs_error: certificate, + tier: NIG_QUADRATURE_NODES as u8, + }) +} + +/// Compatibility NIG call API (`q = 0`, default `$0.005 / $100` request). /// -/// # CU cost -/// ~302,000 CU (native only — exceeds on-chain limits). +/// New integrations should use [`nig_price_certified`] so signed rates, +/// dividends, and the accepted error allowance are explicit. pub fn nig_call_price( s: u128, k: u128, @@ -51,137 +669,270 @@ pub fn nig_call_price( beta: i128, delta: u128, ) -> Result { - if s > i128::MAX as u128 || k > i128::MAX as u128 || r > i128::MAX as u128 - || t > i128::MAX as u128 || alpha > i128::MAX as u128 || delta > i128::MAX as u128 + if s > i128::MAX as u128 + || k > i128::MAX as u128 + || r > i128::MAX as u128 + || t > i128::MAX as u128 + || alpha > i128::MAX as u128 + || delta > i128::MAX as u128 { return Err(SolMathError::Overflow); } - if s == 0 || k == 0 || alpha == 0 || delta == 0 { - return Err(SolMathError::DomainError); - } - // Domain: alpha ≤ 10,000. Real NIG calibrations on equity markets have alpha in [1, 100]. - // This guard prevents complex arithmetic overflow in nig_char_func (see pen test audit). - if alpha > 10_000 * SCALE { - return Err(SolMathError::DomainError); + if t == 0 { + if s == 0 || k == 0 { + return Err(SolMathError::DomainError); + } + return Ok(s.saturating_sub(k)); } + let requested = fp_mul(s.max(k), NIG_DEFAULT_ERROR_REL)?.max(1); + Ok(nig_price_certified( + s, + k, + r as i128, + 0, + t, + NigParams { + alpha, + beta, + delta_per_year: delta, + }, + requested, + )? + .call) +} - let alpha_i = alpha as i128; - let delta_i = delta as i128; - let r_i = r as i128; - let t_i = t as i128; - // NIG parameters - let alpha_sq = fp_mul_i(alpha_i, alpha_i)?; - let beta_sq = fp_mul_i(beta, beta)?; +#[cfg(test)] +mod tests { + use super::*; - // Domain check: NIG requires α > |β| and α > |β+1| - if alpha_sq <= beta_sq { - return Err(SolMathError::DomainError); // invalid: |β| ≥ α + #[test] + fn expiry_is_intrinsic() { + let params = NigParams { + alpha: 10 * SCALE, + beta: -2 * SCALE_I, + delta_per_year: SCALE, + }; + let quote = nig_price_certified(120 * SCALE, 100 * SCALE, 0, 0, 0, params, 0).unwrap(); + assert_eq!(quote.call, 20 * SCALE); + assert_eq!(quote.put, 0); + assert_eq!(quote.max_abs_error, 0); + assert_eq!(quote.tier, 0); } - let gamma = fp_sqrt((alpha_sq - beta_sq) as u128)? as i128; - let gamma_cu = fp_mul_i(fp_mul_i(gamma, gamma)?, gamma)?; // γ³ - // Convexity correction: ω = δ·(γ − √(α²−(β+1)²)) - let bp1 = beta + SCALE_I; - let bp1_sq = fp_mul_i(bp1, bp1)?; - if alpha_sq <= bp1_sq { - return Err(SolMathError::DomainError); // invalid: |β+1| ≥ α + #[test] + fn rejects_insufficient_stock_measure_headroom() { + let params = NigParams { + alpha: 10 * SCALE, + beta: 8 * SCALE_I, + delta_per_year: SCALE, + }; + assert_eq!( + nig_price_certified( + 100 * SCALE, + 100 * SCALE, + 0, + 0, + SCALE, + params, + 20_000_000_000, + ), + Err(SolMathError::DomainError) + ); } - let omega = fp_mul_i( - delta_i, - gamma - fp_sqrt((alpha_sq - bp1_sq) as u128)? as i128, - )?; - // NIG mean and variance of log-price over period T - // c1 = ln(S) + (r−ω)T + δTβ/γ - let ln_s = ln_fixed_i(s)?; - let drift_rate = r_i - omega; - let c1 = - ln_s + fp_mul_i(drift_rate, t_i)? + fp_div_i(fp_mul_i(fp_mul_i(delta_i, t_i)?, beta)?, gamma)?; - // c2 = δTα²/γ³ - let c2 = fp_div_i(fp_mul_i(fp_mul_i(delta_i, t_i)?, alpha_sq)?, gamma_cu)?; - let nig_std = fp_sqrt(c2 as u128)? as i128; - // Truncation range [a, b] - let log_k = ln_fixed_i(k)?; - let l_std = fp_mul_i(NIG_COS_L, nig_std)?; - let mut a = c1 - l_std; - let mut b = c1 + l_std; - // Extend to cover strike with 1-std margin - if log_k - nig_std < a { - a = log_k - nig_std; - } - if log_k + nig_std > b { - b = log_k + nig_std; - } - let ba = b - a; - - let discount = exp_fixed_i(-fp_mul_i(r_i, t_i)?)?; - let exp_b = exp_fixed_i(b)?; // exp(b) - // Precompute drift for char func: ln(S) + (r−ω)T - let cf_drift = ln_s + fp_mul_i(drift_rate, t_i)?; - let delta_t = fp_mul_i(delta_i, t_i)?; - - // COS expansion: Σ_{k=0}^{N-1} ' Re[φ(kπ/(b-a)) · e^{-ikπa/(b-a)}] · V_k - // where ' means k=0 term halved - let mut total: i128 = 0; - let mut i = 0; - while i < NIG_COS_N { - // Frequency: w = i·π / (b-a) — but i is the loop counter, use as integer - // w_ba_num = i (integer), w_ba_den = ba/π - // In SCALE: w = i * PI_SCALE / ba (but careful about overflow) - let w = if i == 0 { - 0i128 - } else { - // i * π / (b-a): compute as fp_div_i(i * PI_SCALE, ba) - fp_div_i((i as i128) * PI_SCALE, ba)? - }; + #[test] + fn scaled_bessel_k1_reference_points() { + // exp(x) K1(x), references rounded from 100-digit mpmath. + let cases = [ + (SCALE / 10, 10_890_182_683_050u128), + (SCALE, 1_636_153_486_263u128), + (10 * SCALE, 410_766_570_595u128), + ]; + for (x, expected) in cases { + let actual = bessel_k1_scaled(x).unwrap(); + assert!( + actual.abs_diff(expected) <= expected / 10_000_000 + 64, + "x={x}, actual={actual}" + ); + } + } - // Characteristic function term - let char_term = if i == 0 { - SCALE_I // φ(0) = 1 - } else { - // Re[φ(w) · exp(-i·w·a)] - let phi = nig_char_func(w, cf_drift, delta_t, gamma, alpha_sq, beta)?; - // exp(-i·w·a) = cos(w·a) − i·sin(w·a) - let wa = fp_mul_i(w, a)?; - let rot = Complex::new(cos_fixed(wa)?, -sin_fixed(wa)?); - complex_mul(phi, rot)?.re + #[test] + fn representative_prices_match_independent_density_integration() { + let cases = [ + // s, k, r, q, t, alpha, beta, delta, call, put + ( + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + 20_000_000_000, + SCALE, + 10 * SCALE, + -2 * SCALE_I, + 200_000_000_000, + 6_892_015_108_422, + 3_995_090_227_818, + ), + ( + 80 * SCALE, + 100 * SCALE, + 30_000_000_000, + 10_000_000_000, + SCALE / 2, + 8 * SCALE, + -3 * SCALE_I, + 400_000_000_000, + 533_653_027_759, + 19_443_848_652_651, + ), + ( + 130 * SCALE, + 100 * SCALE, + -10_000_000_000, + 40_000_000_000, + 2 * SCALE, + 12 * SCALE, + 2 * SCALE_I, + 300_000_000_000, + 21_337_043_419_354, + 3_352_052_391_767, + ), + ]; + for (s, k, r, q, t, alpha, beta, delta, expected_call, expected_put) in cases { + let quote = nig_price_certified( + s, + k, + r, + q, + t, + NigParams { + alpha, + beta, + delta_per_year: delta, + }, + 20_000_000_000, + ) + .unwrap(); + assert!( + quote.call.abs_diff(expected_call) <= quote.max_abs_error, + "quote={quote:?}, expected_call={expected_call}" + ); + assert!( + quote.put.abs_diff(expected_put) <= quote.max_abs_error, + "quote={quote:?}, expected_put={expected_put}" + ); + } + } + + #[test] + fn declared_numerical_boundaries_fail_closed() { + let valid = NigParams { + alpha: 10 * SCALE, + beta: -2 * SCALE_I, + delta_per_year: SCALE / 5, }; + assert_eq!( + nig_price_certified( + 100 * SCALE, + 100 * SCALE, + 0, + 0, + SCALE, + NigParams { + delta_per_year: NIG_MIN_DELTA_TIME - 1, + ..valid + }, + SCALE / 100, + ), + Err(SolMathError::DomainError) + ); + assert_eq!( + nig_price_certified( + 100 * SCALE, + 100 * SCALE, + 0, + 0, + SCALE, + NigParams { + beta: 5 * SCALE_I + SCALE_I / 2 + 1, + ..valid + }, + SCALE / 100, + ), + Err(SolMathError::DomainError) + ); + } - // Payoff coefficients V_k for call: 2/(b-a) × (χ_k − K·ψ_k) - // where c = ln(K), d = b - // For k=0: χ = exp(b) − K, ψ = b − ln(K) - // For k>0: sin(kπ(d−a)/(b−a)) = sin(kπ) = 0, cos(kπ) = (−1)^k - // θ = w·(ln(K)−a) - // χ = ((−1)^k·exp(b) − K·(cos(θ)+w·sin(θ))) / (1+w²) - // ψ = −sin(θ)/w - let v_k = if i == 0 { - // 2/(b-a) × (exp(b) − K − K·(b − ln(K))) - let chi = exp_b as i128 - (k as i128); - let psi = b - log_k; - fp_div_i(2 * (chi - fp_mul_i(k as i128, psi)?), ba)? - } else { - let theta = fp_mul_i(w, log_k - a)?; - let cos_t = cos_fixed(theta)?; - let sin_t = sin_fixed(theta)?; - let w_sq = fp_mul_i(w, w)?; - let sign_k: i128 = if i % 2 == 0 { 1 } else { -1 }; - let chi = fp_div_i( - sign_k * (exp_b as i128) - fp_mul_i(k as i128, cos_t + fp_mul_i(w, sin_t)?)?, - SCALE_I + w_sq, - )?; - let psi = -fp_div_i(sin_t, w)?; - fp_div_i(2 * (chi - fp_mul_i(k as i128, psi)?), ba)? + #[test] + fn strike_shape_and_homogeneity_are_preserved() { + let params = NigParams { + alpha: 10 * SCALE, + beta: -2 * SCALE_I, + delta_per_year: SCALE / 5, }; + let mut calls = [0u128; 3]; + for (index, strike) in [80 * SCALE, 100 * SCALE, 120 * SCALE] + .into_iter() + .enumerate() + { + calls[index] = nig_price_certified( + 100 * SCALE, + strike, + SCALE_I / 20, + SCALE_I / 50, + SCALE, + params, + SCALE / 10, + ) + .unwrap() + .call; + } + assert!(calls[0] >= calls[1] && calls[1] >= calls[2]); + assert!(calls[0] + calls[2] >= 2 * calls[1]); - let weight: i128 = if i == 0 { SCALE_I / 2 } else { SCALE_I }; - total += fp_mul_i(weight, fp_mul_i(char_term, v_k)?)?; - i += 1; + let base = nig_price_certified( + 100 * SCALE, + 100 * SCALE, + SCALE_I / 20, + SCALE_I / 50, + SCALE, + params, + SCALE / 10, + ) + .unwrap(); + let doubled = nig_price_certified( + 200 * SCALE, + 200 * SCALE, + SCALE_I / 20, + SCALE_I / 50, + SCALE, + params, + SCALE / 5, + ) + .unwrap(); + assert!(doubled.call.abs_diff(2 * base.call) <= 2); + assert!(doubled.put.abs_diff(2 * base.put) <= 2); } - let call_i = fp_mul_i(discount, total)?; - Ok(if call_i > 0 { - call_i as u128 - } else { - 0 - }) + #[test] + fn symmetric_large_alpha_case_approaches_black_scholes() { + // alpha=100, beta=-1/2 removes the leading skew under the martingale + // measure. delta=alpha*sigma^2 gives the sigma=20% Brownian limit. + let quote = nig_price_certified( + 100 * SCALE, + 100 * SCALE, + SCALE_I / 20, + 0, + SCALE, + NigParams { + alpha: 100 * SCALE, + beta: -SCALE_I / 2, + delta_per_year: 4 * SCALE, + }, + SCALE, + ) + .unwrap(); + let black_scholes = 10_450_583_572_186u128; + assert!(quote.call.abs_diff(black_scholes) <= 5_000_000_000); + } } diff --git a/src/norm_cdf_coeffs.rs b/src/norm_cdf_coeffs.rs new file mode 100644 index 0000000..3c4edbf --- /dev/null +++ b/src/norm_cdf_coeffs.rs @@ -0,0 +1,179 @@ +// @generated by scripts/generate_norm_cdf_coeffs.py; do not edit manually. +// Coefficients carry 23 binary guard bits and use a Q44 normalized argument. + +pub(crate) const CDF_T_Q: u32 = 44; +pub(crate) const CDF_COEFF_GUARD_Q: u32 = 23; +pub(crate) const CDF_TAIL_EVAL_EXTRA_Q: u32 = 16; + +#[rustfmt::skip] +pub(crate) const NORM_CDF_0_05_Q23: [i64; 9] = [ + 5_022_312_673_274_379_417, + 810_901_814_394_960_375, + -25_340_681_696_364_488, + -7_918_961_773_429_722, + 387_699_146_054_352, + 69_389_502_385_790, + -3_953_372_558_958, + -474_962_914_137, + 29_820_638_421, +]; + +#[rustfmt::skip] +pub(crate) const NORM_CDF_05_10_Q23: [i64; 9] = [ + 6_487_519_978_832_584_408, + 631_530_968_182_396_384, + -59_206_028_255_022_718, + -2_878_071_673_133_046, + 751_638_949_715_500, + -1_201_467_934_009, + -6_225_724_801_139, + 171_608_579_074, + 37_183_908_071, +]; + +#[rustfmt::skip] +pub(crate) const NORM_CDF_10_15_Q23: [i64; 9] = [ + 7_502_353_463_420_028_651, + 383_042_894_792_852_907, + -59_850_452_304_324_548, + 2_244_390_917_043_581, + 448_099_143_512_382, + -49_043_605_770_017, + -1_179_803_424_747, + 412_572_290_876, + -8_152_741_272, +]; + +#[rustfmt::skip] +pub(crate) const NORM_CDF_15_20_Q23: [i64; 9] = [ + 8_052_567_436_258_929_045, + 180_936_651_712_933_008, + -39_579_892_569_961_957, + 3_887_311_126_932_774, + -12_884_030_774_286, + -35_317_084_449_848, + 2_682_329_741_260, + 96_322_531_051, + -22_929_892_322, +]; + +#[rustfmt::skip] +pub(crate) const NORM_CDF_20_25_Q23: [i64; 9] = [ + 8_286_061_690_890_110_763, + 66_562_874_288_895_850, + -18_720_808_403_019_822, + 2_816_788_802_738_217, + -201_102_451_387_864, + -3_785_171_905_445, + 2_030_606_829_142, + -132_607_678_277, + -4_187_729_001, +]; + +#[rustfmt::skip] +pub(crate) const NORM_CDF_25_30_Q23: [i64; 9] = [ + 8_363_611_934_288_315_465, + 19_070_582_784_364_442, + -6_555_512_831_727_815, + 1_303_653_159_784_261, + -155_778_806_952_001, + 9_197_693_666_915, + 244_306_086_201, + -92_237_248_087, + 6_216_213_178, +]; + +#[rustfmt::skip] +pub(crate) const NORM_CDF_30_35_Q23: [i64; 9] = [ + 8_383_767_563_113_200_472, + 4_255_222_200_498_977, + -1_728_684_015_360_267, + 423_859_902_448_590, + -68_089_440_955_744, + 7_091_280_908_713, + -392_810_614_150, + -7_745_913_483, + 3_363_348_037, +]; + +#[rustfmt::skip] +pub(crate) const NORM_CDF_35_40_Q23: [i64; 8] = [ + 8_387_866_302_053_877_089, + 739_446_741_208_920, + -346_615_654_889_821, + 100_614_799_710_785, + -19_971_041_118_753, + 2_801_420_617_745, + -271_251_106_815, + 15_299_614_663, +]; + +#[rustfmt::skip] +pub(crate) const NORM_CDF_40_45_Q23: [i64; 8] = [ + 8_388_518_338_151_337_464, + 100_073_233_982_094, + -53_164_038_770_139, + 17_786_456_193_782, + -4_170_069_803_186, + 719_520_944_996, + -93_728_901_152, + 8_731_131_264, +]; + +#[rustfmt::skip] +pub(crate) const NORM_CDF_45_50_Q23: [i64; 8] = [ + 8_388_599_468_088_094_501, + 10_547_641_148_047, + -6_262_720_284_500, + 2_369_102_993_018, + -637_798_948_297, + 129_316_723_829, + -20_745_949_957, + 2_509_157_234, +]; + +#[rustfmt::skip] +pub(crate) const NORM_TAIL_50_55_Q23: [i64; 7] = [ + 637_950_326_283, + -865_853_490_422, + 568_189_218_107, + -239_158_995_636, + 72_640_182_033, + -17_636_074_988, + 3_171_254_302, +]; + +#[rustfmt::skip] +pub(crate) const NORM_TAIL_55_60_Q23: [i64; 7] = [ + 37_442_282_177, + -55_350_763_209, + 39_586_705_805, + -18_452_922_552, + 6_745_935_486, + -1_704_412_420, + 0, +]; + +#[rustfmt::skip] +pub(crate) const NORM_TAIL_60_65_Q23: [i64; 7] = [ + 1_721_563_339, + -2_717_324_504, + 2_144_137_258, + -1_244_276_484, + 438_993_263, + 0, + 0, +]; + +#[rustfmt::skip] +pub(crate) const NORM_TAIL_65_70_Q23: [i64; 7] = [ + 59_305_874, + -106_020_191, + 111_584_833, + -56_182_006, + 0, + 0, + 0, +]; + +pub(crate) const NORM_TAIL_HALF_RAW_CUTOFF: i128 = 7_130_506_848_171; diff --git a/src/normal.rs b/src/normal.rs index ef961e5..7c4e2a8 100644 --- a/src/normal.rs +++ b/src/normal.rs @@ -1,6 +1,7 @@ +use crate::arithmetic::{fp_div_i, fp_mul_i, fp_mul_i_round, fp_sqrt}; use crate::constants::*; use crate::error::SolMathError; -use crate::arithmetic::{fp_mul_i, fp_mul_i_round, fp_div_i, fp_div_i_round, fp_sqrt}; +use crate::norm_cdf_coeffs::*; use crate::transcendental::{exp_fixed_i, ln_fixed_i}; /// Standard normal PDF: phi(x) = (1/sqrt(2*pi)) * exp(-x^2/2) at SCALE. @@ -10,10 +11,17 @@ use crate::transcendental::{exp_fixed_i, ln_fixed_i}; /// - **Errors**: `Overflow` on internal arithmetic overflow (extremely unlikely). /// - **Accuracy**: max 2 ULP. pub fn norm_pdf(x: i128) -> Result { + // Extreme |x|: the pdf underflows to 0 long before 9σ. Short-circuit + // before squaring so huge |x| (up to i128::MIN/MAX) returns Ok(0) as + // documented instead of an internal Overflow. Written as two compares — + // x.abs() would overflow for x == i128::MIN. + if x > 9 * SCALE_I || x < -9 * SCALE_I { + return Ok(0); + } let x_sq = fp_mul_i(x, x)?; - // x_sq ∈ [0, 64·SCALE_I²/SCALE_I] = [0, ~64·SCALE_I] after fp_mul_i; /2 and negate: result ∈ [-32·SCALE_I, 0], fits i128. + // |x| ≤ 9·SCALE_I after the guard, so x_sq ≤ 81·SCALE_I; /2 and negate: result ∈ [-41·SCALE_I, 0], fits i128. let neg_half_x_sq = -(x_sq / 2); - // Guard: for extreme |x|, -x²/2 underflows past exp's range → pdf is 0. + // Guard: for 8.94 < |x| ≤ 9, -x²/2 underflows past exp's range → pdf is 0. if neg_half_x_sq < -40 * SCALE_I { return Ok(0); } @@ -25,77 +33,195 @@ pub fn norm_pdf(x: i128) -> Result { fp_mul_i(INV_SQRT_2PI, exp_term) } -/// Rounding Horner degree-11. Uses fp_mul_i_round instead of fp_mul_i. +#[inline(always)] +fn round_shift_cdf(value: i128) -> i64 { + const HALF: i128 = 1i128 << (CDF_T_Q - 1); + if value >= 0 { + ((value + HALF) >> CDF_T_Q) as i64 + } else { + -(((-value + HALF) >> CDF_T_Q) as i64) + } +} + #[inline] -pub(crate) fn horner_11_round(c: &[i128; 12], t: i128) -> Result { - let mut r = c[11]; - // Each step: fp_mul_i_round result ∈ [-SCALE_I, SCALE_I] (r and t are both ≤ SCALE_I after initial - // coefficient), coefficients |c[i]| ≤ SCALE_I; sum ∈ [-2·SCALE_I, 2·SCALE_I], fits i128. - r = fp_mul_i_round(r, t)? + c[10]; - r = fp_mul_i_round(r, t)? + c[9]; - r = fp_mul_i_round(r, t)? + c[8]; - r = fp_mul_i_round(r, t)? + c[7]; - r = fp_mul_i_round(r, t)? + c[6]; - r = fp_mul_i_round(r, t)? + c[5]; - r = fp_mul_i_round(r, t)? + c[4]; - r = fp_mul_i_round(r, t)? + c[3]; - r = fp_mul_i_round(r, t)? + c[2]; - r = fp_mul_i_round(r, t)? + c[1]; - r = fp_mul_i_round(r, t)? + c[0]; - Ok(r) +fn horner_guard_q44(coefficients: &[i64; N], t: i64) -> i64 { + let mut result = coefficients[N - 1]; + for coefficient in coefficients[..N - 1].iter().rev() { + result = round_shift_cdf(result as i128 * t as i128) + coefficient; + } + let half = 1i64 << (CDF_COEFF_GUARD_Q - 1); + if result >= 0 { + (result + half) >> CDF_COEFF_GUARD_Q + } else { + -((-result + half) >> CDF_COEFF_GUARD_Q) + } } -/// Rounding map: (|x| - mid) × SCALE / hw, rounded to nearest. +/// Evaluate a guarded CDF polynomial and its derivative with respect to its +/// normalized coordinate. The value path is deliberately bit-for-bit the +/// same Horner recurrence as `horner_guard_q44`. #[inline] -pub(crate) fn poly_map_t_round(ax: i128, mid: i128, hw: i128) -> Result { - // ax ∈ [0, 8·SCALE_I], mid ∈ [0, 5·SCALE_I]: ax - mid ∈ [-5·SCALE_I, 8·SCALE_I], fits i128. - // checked_mul guards the ×SCALE_I step. hw/2: hw is a small constant ≤ SCALE_I, fits i128. - let num = (ax - mid).checked_mul(SCALE_I).ok_or(SolMathError::Overflow)?; - Ok(if num >= 0 { (num + hw / 2) / hw } else { (num - hw / 2) / hw }) +fn horner_guard_q44_with_derivative(coefficients: &[i64; N], t: i64) -> (i64, i64) { + let mut value = coefficients[N - 1]; + let mut derivative = 0i64; + for coefficient in coefficients[..N - 1].iter().rev() { + derivative = round_shift_cdf(derivative as i128 * t as i128) + value; + value = round_shift_cdf(value as i128 * t as i128) + coefficient; + } + let half = 1i64 << (CDF_COEFF_GUARD_Q - 1); + let rounded_value = if value >= 0 { + (value + half) >> CDF_COEFF_GUARD_Q + } else { + -((-value + half) >> CDF_COEFF_GUARD_Q) + }; + (rounded_value, derivative) } -/// Mills ratio via 6-level continued fraction. For |x| ≥ 5 at SCALE. -/// At x=5, CF6 vs CF8 difference is < 0.001 ULP. Saves ~2K CU. +/// Tail Horner evaluation with 16 evaluation-only guard bits. +/// +/// The stored coefficients remain Q23 i64 values. Promoting them to Q39 in +/// an i64 accumulator prevents sub-raw tail increments from wobbling across a +/// final rounding threshold, without increasing the coefficient payload or +/// changing the common body path. #[inline] -pub(crate) fn mills_ratio_cf6(x: i128) -> Result { - let mut r = 0i128; - for k in (1..=6).rev() { - // k ∈ [1, 6], SCALE_I = 1e12: k * SCALE_I ≤ 6e12, fits i128. - // r is the previous continued-fraction level (bounded by SCALE_I); x ∈ [5·SCALE_I, 8·SCALE_I]; - // x + r ≤ 9·SCALE_I, fits i128. - r = fp_div_i_round(k * SCALE_I, x + r)?; +fn horner_tail_guard_q44(coefficients: &[i64; N], t: i64) -> i64 { + let mut result = coefficients[N - 1] << CDF_TAIL_EVAL_EXTRA_Q; + for coefficient in coefficients[..N - 1].iter().rev() { + result = + round_shift_cdf(result as i128 * t as i128) + (*coefficient << CDF_TAIL_EVAL_EXTRA_Q); + } + + const OUTPUT_Q: u32 = CDF_COEFF_GUARD_Q + CDF_TAIL_EVAL_EXTRA_Q; + const OUTPUT_HALF: i64 = 1i64 << (OUTPUT_Q - 1); + if result >= 0 { + (result + OUTPUT_HALF) >> OUTPUT_Q + } else { + -((-result + OUTPUT_HALF) >> OUTPUT_Q) } - fp_div_i_round(SCALE_I, x + r) } -/// Tail CDF for |x| >= 5×SCALE via asymptotic expansion. -/// Φ(x) = SCALE − φ(x) × mills_ratio(x). Naturally monotone. -pub(crate) fn norm_cdf_tail(x_abs: i128) -> Result { - // φ(x) = exp(-x²/2) / √(2π) - // x_abs ∈ [5·SCALE_I, 8·SCALE_I]; fp_mul_i_round result ∈ [0, 64·SCALE_I]; /2: ∈ [0, 32·SCALE_I], fits i128. - let x_sq_half = fp_mul_i_round(x_abs, x_abs)? / 2; - if x_sq_half > 40 * SCALE_I { - return Ok(SCALE_I); +#[inline] +fn horner_tail_guard_q44_with_derivative( + coefficients: &[i64; N], + t: i64, +) -> (i64, i64) { + let mut value = coefficients[N - 1] << CDF_TAIL_EVAL_EXTRA_Q; + let mut derivative = 0i64; + for coefficient in coefficients[..N - 1].iter().rev() { + derivative = round_shift_cdf(derivative as i128 * t as i128) + value; + value = + round_shift_cdf(value as i128 * t as i128) + (*coefficient << CDF_TAIL_EVAL_EXTRA_Q); } - let exp_val = exp_fixed_i(-x_sq_half)?; - let pdf = fp_mul_i_round(INV_SQRT_2PI, exp_val)?; + const OUTPUT_Q: u32 = CDF_COEFF_GUARD_Q + CDF_TAIL_EVAL_EXTRA_Q; + const OUTPUT_HALF: i64 = 1i64 << (OUTPUT_Q - 1); + let rounded_value = if value >= 0 { + (value + OUTPUT_HALF) >> OUTPUT_Q + } else { + -((-value + OUTPUT_HALF) >> OUTPUT_Q) + }; + (rounded_value, derivative) +} - // tail = φ(x) × R(x) - let mills = mills_ratio_cf6(x_abs)?; - let tail = fp_mul_i_round(pdf, mills)?; +#[inline(always)] +fn derivative_to_pdf(derivative: i64, bits: u32, tail: bool) -> i128 { + // Every CDF interval has half-width 0.25, hence dx/dt = 0.25 and + // d/dx = 4 d/dt. Tail polynomials approximate Phi(-x), so negate. + let signed = if tail { + -(derivative as i128) + } else { + derivative as i128 + }; + let value = signed * 4; + let half = 1i128 << (bits - 1); + let rounded = if value >= 0 { + (value + half) >> bits + } else { + -((-value + half) >> bits) + }; + rounded.clamp(0, SCALE_I) +} - // tail ∈ [0, SCALE_I] (pdf and mills both bounded); SCALE_I - tail ∈ [0, SCALE_I], fits i128. - Ok((SCALE_I - tail).clamp(0, SCALE_I)) +/// Map `(|x|-mid)/half_width` to Q44 with a Q44 reciprocal guard. +#[inline(always)] +pub(crate) fn poly_map_t_q44(ax: i128, mid: i128, hw: i128) -> i64 { + // round(2^88 / half_width), followed by a 44-bit shift, produces Q44. + // The reciprocal error contributes <0.015 Q44 units at |t|≤1. + let reciprocal = match hw { + 250_000_000_000 => 1_237_940_039_285_380i128, + 375_000_000_000 => 825_293_359_523_587i128, + 500_000_000_000 => 618_970_019_642_690i128, + // All current callers use one of the three optimized constants above. + // Keep the helper total so a future internal caller cannot introduce a + // runtime panic. `hw <= 0` maps to zero and is harmless because the + // mapped coordinate is used only by fixed, positive-width intervals. + _ if hw > 0 => ((1i128 << 88) + hw / 2) / hw, + _ => 0, + }; + round_shift_cdf((ax - mid) * reciprocal) +} + +/// Direct positive CDF tail for `5 < x <= 8`. +#[inline] +fn norm_cdf_positive_tail(x_abs: i128) -> i128 { + let tail = if x_abs <= 11 * SCALE_I / 2 { + let t = poly_map_t_q44(x_abs, 21 * SCALE_I / 4, SCALE_I / 4); + horner_tail_guard_q44(&NORM_TAIL_50_55_Q23, t) + } else if x_abs <= 6 * SCALE_I { + let t = poly_map_t_q44(x_abs, 23 * SCALE_I / 4, SCALE_I / 4); + horner_tail_guard_q44(&NORM_TAIL_55_60_Q23, t) + } else if x_abs <= 13 * SCALE_I / 2 { + let t = poly_map_t_q44(x_abs, 25 * SCALE_I / 4, SCALE_I / 4); + horner_tail_guard_q44(&NORM_TAIL_60_65_Q23, t) + } else if x_abs <= 7 * SCALE_I { + let t = poly_map_t_q44(x_abs, 27 * SCALE_I / 4, SCALE_I / 4); + horner_tail_guard_q44(&NORM_TAIL_65_70_Q23, t) + } else if x_abs <= NORM_TAIL_HALF_RAW_CUTOFF { + 1 + } else { + 0 + }; + SCALE_I - tail.clamp(0, SCALE_I as i64) as i128 +} + +#[inline] +fn norm_cdf_positive_tail_and_pdf(x_abs: i128) -> (i128, i128) { + const TAIL_Q: u32 = CDF_COEFF_GUARD_Q + CDF_TAIL_EVAL_EXTRA_Q; + let (tail, derivative) = if x_abs <= 11 * SCALE_I / 2 { + let t = poly_map_t_q44(x_abs, 21 * SCALE_I / 4, SCALE_I / 4); + horner_tail_guard_q44_with_derivative(&NORM_TAIL_50_55_Q23, t) + } else if x_abs <= 6 * SCALE_I { + let t = poly_map_t_q44(x_abs, 23 * SCALE_I / 4, SCALE_I / 4); + horner_tail_guard_q44_with_derivative(&NORM_TAIL_55_60_Q23, t) + } else if x_abs <= 13 * SCALE_I / 2 { + let t = poly_map_t_q44(x_abs, 25 * SCALE_I / 4, SCALE_I / 4); + horner_tail_guard_q44_with_derivative(&NORM_TAIL_60_65_Q23, t) + } else if x_abs <= 7 * SCALE_I { + let t = poly_map_t_q44(x_abs, 27 * SCALE_I / 4, SCALE_I / 4); + horner_tail_guard_q44_with_derivative(&NORM_TAIL_65_70_Q23, t) + } else { + let tail = if x_abs <= NORM_TAIL_HALF_RAW_CUTOFF { + 1 + } else { + 0 + }; + return (SCALE_I - tail, 0); + }; + ( + SCALE_I - (tail as i128).clamp(0, SCALE_I), + derivative_to_pdf(derivative, TAIL_Q, true), + ) } /// Standard normal CDF: Phi(x) at SCALE. /// -/// 6-piece minimax polynomial + continued-fraction asymptotic tail. +/// 10-piece guarded body + four direct guarded tail polynomials. /// /// - **x**: signed fixed-point at `SCALE` (1e12). /// - **Returns**: `i128` probability in [0, SCALE_I]. Returns 0 for x < -8*SCALE, SCALE_I for x > 8*SCALE. /// - **Errors**: `Overflow` on internal arithmetic overflow (extremely unlikely). -/// - **Accuracy**: max 4 ULP, 50% exact. Monotone with zero boundary discontinuity. +/// - **Accuracy**: max 2 ULP on the retained 100K production and 10K +/// seam-focused adversarial corpora; exactly symmetric and monotone on the +/// generator's dense interval sweeps. pub fn norm_cdf_poly(x: i128) -> Result { if x < -8 * SCALE_I { return Ok(0); @@ -109,43 +235,345 @@ pub fn norm_cdf_poly(x: i128) -> Result { let ax = x.abs(); - let cdf_pos = if ax <= POLY_V2_I0_HI { - horner_11_round(&POLY_V2_I0, poly_map_t_round(ax, POLY_V2_I0_MID, POLY_V2_I0_HW)?)? - } else if ax <= POLY_V2_I1_HI { - horner_11_round(&POLY_V2_I1, poly_map_t_round(ax, POLY_V2_I1_MID, POLY_V2_I1_HW)?)? - } else if ax <= POLY_V2_I2_HI { - horner_11_round(&POLY_V2_I2, poly_map_t_round(ax, POLY_V2_I2_MID, POLY_V2_I2_HW)?)? - } else if ax <= POLY_V2_I3_HI { - horner_11_round(&POLY_V2_I3, poly_map_t_round(ax, POLY_V2_I3_MID, POLY_V2_I3_HW)?)? - } else if ax <= POLY_V2_I4_HI { - horner_11_round(&POLY_V2_I4, poly_map_t_round(ax, POLY_V2_I4_MID, POLY_V2_I4_HW)?)? + // A balanced decision tree holds the seven degree-8 body pieces to at + // most four comparisons. A sequential chain made upper-body and tail + // inputs pay for every earlier interval (~8 CU per comparison on SBF). + let cdf_pos = if ax <= 7 * SCALE_I / 2 { + if ax <= 3 * SCALE_I / 2 { + if ax <= SCALE_I / 2 { + horner_guard_q44( + &NORM_CDF_0_05_Q23, + poly_map_t_q44(ax, SCALE_I / 4, SCALE_I / 4), + ) as i128 + } else if ax <= SCALE_I { + horner_guard_q44( + &NORM_CDF_05_10_Q23, + poly_map_t_q44(ax, 3 * SCALE_I / 4, SCALE_I / 4), + ) as i128 + } else { + horner_guard_q44( + &NORM_CDF_10_15_Q23, + poly_map_t_q44(ax, 5 * SCALE_I / 4, SCALE_I / 4), + ) as i128 + } + } else if ax <= 5 * SCALE_I / 2 { + if ax <= 2 * SCALE_I { + horner_guard_q44( + &NORM_CDF_15_20_Q23, + poly_map_t_q44(ax, 7 * SCALE_I / 4, SCALE_I / 4), + ) as i128 + } else { + horner_guard_q44( + &NORM_CDF_20_25_Q23, + poly_map_t_q44(ax, 9 * SCALE_I / 4, SCALE_I / 4), + ) as i128 + } + } else if ax <= 3 * SCALE_I { + horner_guard_q44( + &NORM_CDF_25_30_Q23, + poly_map_t_q44(ax, 11 * SCALE_I / 4, SCALE_I / 4), + ) as i128 + } else { + horner_guard_q44( + &NORM_CDF_30_35_Q23, + poly_map_t_q44(ax, 13 * SCALE_I / 4, SCALE_I / 4), + ) as i128 + } } else if ax <= 5 * SCALE_I { - horner_11_round(&POLY_V2_I5, poly_map_t_round(ax, POLY_V2_I5_MID, POLY_V2_I5_HW)?)? + if ax <= 4 * SCALE_I { + horner_guard_q44( + &NORM_CDF_35_40_Q23, + poly_map_t_q44(ax, 15 * SCALE_I / 4, SCALE_I / 4), + ) as i128 + } else if ax <= 9 * SCALE_I / 2 { + horner_guard_q44( + &NORM_CDF_40_45_Q23, + poly_map_t_q44(ax, 17 * SCALE_I / 4, SCALE_I / 4), + ) as i128 + } else { + horner_guard_q44( + &NORM_CDF_45_50_Q23, + poly_map_t_q44(ax, 19 * SCALE_I / 4, SCALE_I / 4), + ) as i128 + } } else { - // Asymptotic tail: Φ(x) = SCALE − φ(x) × mills_ratio(x) - norm_cdf_tail(ax)? + norm_cdf_positive_tail(ax) }; let cdf_pos = cdf_pos.clamp(0, SCALE_I); // cdf_pos ∈ [0, SCALE_I] after clamp; SCALE_I - cdf_pos ∈ [0, SCALE_I], fits i128. - Ok(if x >= 0 { - cdf_pos + Ok(if x >= 0 { cdf_pos } else { SCALE_I - cdf_pos }) +} + +/// Direct polynomial CDF together with the analytic derivative of the active +/// CDF piece as an inexpensive PDF approximation. +/// +/// The CDF result is bit-identical to [`norm_cdf_poly`]. On `|x| <= 5`, the +/// derived PDF differs from the exponential reference by less than 500 raw +/// SCALE units (5e-10 absolute). This path is intended for iterative kernels +/// such as the American-option smooth-pasting equation, where a full +/// exponential for every Jacobian sample would dominate compute. +pub fn norm_cdf_and_pdf_poly(x: i128) -> Result<(i128, i128), SolMathError> { + if x < -8 * SCALE_I { + return Ok((0, 0)); + } + if x > 8 * SCALE_I { + return Ok((SCALE_I, 0)); + } + if x == 0 { + return Ok((SCALE_I / 2, INV_SQRT_2PI)); + } + + macro_rules! body_piece { + ($coefficients:expr, $midpoint:expr, $half_width:expr) => {{ + let t = poly_map_t_q44(x.abs(), $midpoint, $half_width); + let (cdf, derivative) = horner_guard_q44_with_derivative($coefficients, t); + ( + cdf as i128, + derivative_to_pdf(derivative, CDF_COEFF_GUARD_Q, false), + ) + }}; + } + + let ax = x.abs(); + let (cdf_pos, pdf) = if ax <= 7 * SCALE_I / 2 { + if ax <= 3 * SCALE_I / 2 { + if ax <= SCALE_I / 2 { + body_piece!(&NORM_CDF_0_05_Q23, SCALE_I / 4, SCALE_I / 4) + } else if ax <= SCALE_I { + body_piece!(&NORM_CDF_05_10_Q23, 3 * SCALE_I / 4, SCALE_I / 4) + } else { + body_piece!(&NORM_CDF_10_15_Q23, 5 * SCALE_I / 4, SCALE_I / 4) + } + } else if ax <= 5 * SCALE_I / 2 { + if ax <= 2 * SCALE_I { + body_piece!(&NORM_CDF_15_20_Q23, 7 * SCALE_I / 4, SCALE_I / 4) + } else { + body_piece!(&NORM_CDF_20_25_Q23, 9 * SCALE_I / 4, SCALE_I / 4) + } + } else if ax <= 3 * SCALE_I { + body_piece!(&NORM_CDF_25_30_Q23, 11 * SCALE_I / 4, SCALE_I / 4) + } else { + body_piece!(&NORM_CDF_30_35_Q23, 13 * SCALE_I / 4, SCALE_I / 4) + } + } else if ax <= 5 * SCALE_I { + if ax <= 4 * SCALE_I { + body_piece!(&NORM_CDF_35_40_Q23, 15 * SCALE_I / 4, SCALE_I / 4) + } else if ax <= 9 * SCALE_I / 2 { + body_piece!(&NORM_CDF_40_45_Q23, 17 * SCALE_I / 4, SCALE_I / 4) + } else { + body_piece!(&NORM_CDF_45_50_Q23, 19 * SCALE_I / 4, SCALE_I / 4) + } } else { - SCALE_I - cdf_pos - }) + norm_cdf_positive_tail_and_pdf(ax) + }; + + let cdf_pos = cdf_pos.clamp(0, SCALE_I); + Ok((if x >= 0 { cdf_pos } else { SCALE_I - cdf_pos }, pdf)) } /// Combined normal CDF and PDF: returns `(Phi(x), phi(x))` at SCALE. /// +/// The CDF uses its direct piecewise polynomial in every interval. The PDF +/// performs the only exponential evaluation needed by this fused call. +/// /// - **x**: signed fixed-point at `SCALE` (1e12). /// - **Returns**: `(i128, i128)` — `(CDF, PDF)` both at `SCALE`. CDF in [0, SCALE_I]. /// - **Errors**: `Overflow` on internal arithmetic overflow. -/// - **Accuracy**: CDF max 4 ULP, PDF max 2 ULP. +/// - **Accuracy**: CDF is bit-identical to `norm_cdf_poly`; PDF is +/// bit-identical to `norm_pdf` (2 ULP max on the retained corpora). pub fn norm_cdf_and_pdf(x: i128) -> Result<(i128, i128), SolMathError> { Ok((norm_cdf_poly(x)?, norm_pdf(x)?)) } +#[cfg(test)] +mod cdf_tests { + use super::*; + + #[test] + fn norm_cdf_is_symmetric_at_all_piece_seams() { + let seams = [ + 0, + SCALE_I / 2, + SCALE_I, + 3 * SCALE_I / 2, + 2 * SCALE_I, + 5 * SCALE_I / 2, + 3 * SCALE_I, + 7 * SCALE_I / 2, + 4 * SCALE_I, + 9 * SCALE_I / 2, + 5 * SCALE_I, + 11 * SCALE_I / 2, + 6 * SCALE_I, + 13 * SCALE_I / 2, + 7 * SCALE_I, + NORM_TAIL_HALF_RAW_CUTOFF, + 8 * SCALE_I, + ]; + for seam in seams { + for offset in -4_096..=4_096 { + let x = seam + offset; + let positive = norm_cdf_poly(x).unwrap(); + let negative = norm_cdf_poly(-x).unwrap(); + assert_eq!(positive + negative, SCALE_I, "symmetry failed at {x}"); + } + } + } + + #[test] + fn norm_cdf_is_monotone_at_seams_and_across_domain() { + let seams = [ + -8 * SCALE_I, + -NORM_TAIL_HALF_RAW_CUTOFF, + -7 * SCALE_I, + -13 * SCALE_I / 2, + -6 * SCALE_I, + -11 * SCALE_I / 2, + -5 * SCALE_I, + -9 * SCALE_I / 2, + -4 * SCALE_I, + -7 * SCALE_I / 2, + -3 * SCALE_I, + -5 * SCALE_I / 2, + -2 * SCALE_I, + -3 * SCALE_I / 2, + -SCALE_I, + -SCALE_I / 2, + 0, + SCALE_I / 2, + SCALE_I, + 3 * SCALE_I / 2, + 2 * SCALE_I, + 5 * SCALE_I / 2, + 3 * SCALE_I, + 7 * SCALE_I / 2, + 4 * SCALE_I, + 9 * SCALE_I / 2, + 5 * SCALE_I, + 11 * SCALE_I / 2, + 6 * SCALE_I, + 13 * SCALE_I / 2, + 7 * SCALE_I, + NORM_TAIL_HALF_RAW_CUTOFF, + 8 * SCALE_I, + ]; + for seam in seams { + let mut previous = norm_cdf_poly(seam - 4_096).unwrap(); + for x in (seam - 4_095)..=(seam + 4_096) { + let value = norm_cdf_poly(x).unwrap(); + assert!( + value >= previous, + "local inversion at {x}: {value} < {previous}" + ); + previous = value; + } + } + + let step = 20_000_003i128; + let mut x = -8 * SCALE_I; + let mut previous = norm_cdf_poly(x).unwrap(); + while x < 8 * SCALE_I { + x = (x + step).min(8 * SCALE_I); + let value = norm_cdf_poly(x).unwrap(); + assert!( + value >= previous, + "domain inversion at {x}: {value} < {previous}" + ); + previous = value; + } + } + + #[test] + fn norm_cdf_q23_guard_prevents_upper_body_rounding_reversal() { + let cases = [ + (4_971_877_961_316, 999_999_668_462), + (4_971_877_961_317, 999_999_668_462), + (4_971_877_961_318, 999_999_668_462), + (4_971_877_961_319, 999_999_668_463), + ]; + let mut previous = 0; + for (x, expected) in cases { + let actual = norm_cdf_poly(x).unwrap(); + assert_eq!(actual, expected, "Q23 regression changed at {x}"); + assert!(actual >= previous, "Q23 rounding reversal at {x}"); + previous = actual; + } + } + + #[test] + fn norm_cdf_q39_tail_guard_prevents_rounding_reversal() { + let cases = [ + (5_447_923_820_214, 999_999_974_519), + (5_447_923_820_215, 999_999_974_519), + (5_447_923_820_216, 999_999_974_520), + (5_447_923_820_217, 999_999_974_520), + (5_447_923_820_218, 999_999_974_520), + ]; + let mut previous = 0; + for (x, expected) in cases { + let actual = norm_cdf_poly(x).unwrap(); + assert_eq!(actual, expected, "Q39 tail regression changed at {x}"); + assert!(actual >= previous, "Q39 tail rounding reversal at {x}"); + previous = actual; + } + } + + #[test] + fn fused_cdf_pdf_uses_public_kernels_exactly() { + for x in (-9_000..=9_000).step_by(17) { + let x = i128::from(x) * 1_000_000_000; + assert_eq!( + norm_cdf_and_pdf(x).unwrap(), + (norm_cdf_poly(x).unwrap(), norm_pdf(x).unwrap()) + ); + } + } + + #[test] + fn polynomial_pdf_reuses_cdf_exactly_and_tracks_density() { + let mut x = -8 * SCALE_I; + while x <= 8 * SCALE_I { + let (cdf, pdf) = norm_cdf_and_pdf_poly(x).unwrap(); + assert_eq!(cdf, norm_cdf_poly(x).unwrap(), "CDF changed at {x}"); + let reference = norm_pdf(x).unwrap(); + assert!( + pdf.abs_diff(reference) <= 600, + "polynomial PDF gap at {x}: {pdf} vs {reference}" + ); + x += 1_000_000_000; + } + + for seam_half in 0..=14 { + let seam = i128::from(seam_half) * SCALE_I / 2; + for offset in [-4_096, -1, 0, 1, 4_096] { + let x = seam + offset; + let (cdf, pdf) = norm_cdf_and_pdf_poly(x).unwrap(); + assert_eq!(cdf, norm_cdf_poly(x).unwrap(), "CDF seam changed at {x}"); + assert!(pdf.abs_diff(norm_pdf(x).unwrap()) <= 600); + } + } + } + + #[test] + fn norm_cdf_retains_two_ulp_regressions() { + let cases = [ + (499_999_999_979, 691_462_461_267), + (-4_079, 499_999_998_373), + (-1_491_753_830_411, 67_881_845_760), + (-5_754_403_847_342, 4_347), + ]; + for (x, expected) in cases { + let actual = norm_cdf_poly(x).unwrap(); + assert!( + actual.abs_diff(expected) <= 2, + "CDF regression at {x}: {actual} vs {expected}" + ); + } + } +} + /// BS-guarded CDF+PDF: short-circuits to (0,0)/(SCALE,0) beyond ±8σ. Internal. #[cfg(feature = "bs")] pub(crate) fn norm_cdf_and_pdf_bs_guarded(x: i128) -> Result<(i128, i128), SolMathError> { @@ -155,15 +583,16 @@ pub(crate) fn norm_cdf_and_pdf_bs_guarded(x: i128) -> Result<(i128, i128), SolMa if x >= 8 * SCALE_I { return Ok((SCALE_I, 0)); } - Ok((norm_cdf_poly(x)?, norm_pdf(x)?)) + norm_cdf_and_pdf(x) } /// Degree-7 Horner evaluation with rounding. #[inline] fn horner_7_round(c: &[i128; 8], r: i128) -> Result { let mut acc = c[7]; - // Each step: fp_mul_i_round result ∈ [-SCALE_I, SCALE_I]; |c[i]| ≤ SCALE_I; - // sum ∈ [-2·SCALE_I, 2·SCALE_I], fits i128. + // Each step: |r| ≤ ~1.6·SCALE_I (AS241 branch variables), so 7 Horner steps + // scale the accumulator by at most ~1.6^7 ≈ 27×. AS241 coefficients reach + // ~6.7e16, so partial sums stay < ~2e18 ≪ i128::MAX (1.7e38). acc = fp_mul_i_round(acc, r)? + c[6]; acc = fp_mul_i_round(acc, r)? + c[5]; acc = fp_mul_i_round(acc, r)? + c[4]; @@ -178,8 +607,8 @@ fn horner_7_round(c: &[i128; 8], r: i128) -> Result { #[inline] fn horner_7_den_round(c: &[i128; 7], r: i128) -> Result { let mut acc = c[6]; - // Each step: fp_mul_i_round result ∈ [-SCALE_I, SCALE_I]; |c[i]| ≤ SCALE_I; - // sum ∈ [-2·SCALE_I, 2·SCALE_I], fits i128. + // Same bound as horner_7_round: |r| ≤ ~1.6·SCALE_I and AS241 coefficients + // reach ~6.7e16, so partial sums stay < ~2e18 ≪ i128::MAX (1.7e38). acc = fp_mul_i_round(acc, r)? + c[5]; acc = fp_mul_i_round(acc, r)? + c[4]; acc = fp_mul_i_round(acc, r)? + c[3]; @@ -253,6 +682,10 @@ pub fn inverse_norm_cdf(p: i128) -> Result { }; // ret is a z-score ∈ [0, ~8·SCALE_I]; negation: ∈ (-8·SCALE_I, 0], fits i128. - if q < 0 { Ok(-ret) } else { Ok(ret) } + if q < 0 { + Ok(-ret) + } else { + Ok(ret) + } } } diff --git a/src/overflow.rs b/src/overflow.rs index d829f8f..5105eb4 100644 --- a/src/overflow.rs +++ b/src/overflow.rs @@ -1,7 +1,9 @@ -use crate::constants::{U256, MulDivRounding}; +use crate::constants::{MulDivRounding, U256}; use crate::error::SolMathError; -/// U256 long division. Internal — fallback for checked_mul_div_rem_u. +/// U256 long division reference used to cross-check optimized division in +/// this crate's own tests. It is intentionally absent from on-chain builds. +#[cfg(test)] #[inline] pub(crate) fn div_rem_u256_long(numerator: U256, divisor: U256) -> (U256, U256) { let mut rem = U256::zero(); @@ -46,6 +48,7 @@ pub(crate) fn checked_mul_div_rem_u(a: u128, b: u128, c: u128) -> Option<(u128, return None; } + #[cfg(test)] debug_assert_eq!( (quo, U256::from_u128(rem)), div_rem_u256_long(numerator, U256::from_u128(c)) @@ -62,7 +65,12 @@ pub(crate) fn checked_mul_div_u(a: u128, b: u128, c: u128) -> Option { /// Overflow-safe signed (a × b) / c with configurable rounding. Internal. #[inline] -pub(crate) fn checked_mul_div_round_i(a: i128, b: i128, c: i128, rounding: MulDivRounding) -> Result { +pub(crate) fn checked_mul_div_round_i( + a: i128, + b: i128, + c: i128, + rounding: MulDivRounding, +) -> Result { if c == 0 { return Err(SolMathError::DivisionByZero); } @@ -79,8 +87,12 @@ pub(crate) fn checked_mul_div_round_i(a: i128, b: i128, c: i128, rounding: MulDi if rem != 0 { match rounding { MulDivRounding::ToZero => {} - MulDivRounding::Floor if neg => mag = mag.checked_add(1).ok_or(SolMathError::Overflow)?, - MulDivRounding::Ceil if !neg => mag = mag.checked_add(1).ok_or(SolMathError::Overflow)?, + MulDivRounding::Floor if neg => { + mag = mag.checked_add(1).ok_or(SolMathError::Overflow)? + } + MulDivRounding::Ceil if !neg => { + mag = mag.checked_add(1).ok_or(SolMathError::Overflow)? + } _ => {} } } diff --git a/src/phi2table.rs b/src/phi2table.rs index d9d51d5..4791229 100644 --- a/src/phi2table.rs +++ b/src/phi2table.rs @@ -1,7 +1,7 @@ -//! Fast bivariate normal CDF at fixed ρ via Catmull-Rom bicubic lookup. +//! Fast bivariate normal CDF at fixed ρ via monotone bilinear lookup. //! -//! The entire evaluation runs in pure i64 arithmetic: 20 multiply-accumulates, -//! 5 shifts, 4 divides for indexing. No `norm_cdf`, no i128 in the hot path. +//! Bilinear interpolation cannot overshoot the four surrounding grid values, +//! preserving a monotone generated table's probability ordering. //! //! # Usage //! @@ -22,64 +22,736 @@ //! let b = 300_000_000_000i128; // 0.3 //! let result = table.eval(a, b)?; //! // result ≈ Φ₂(-0.5, 0.3; 0.75) × SCALE +//! // This is the compatibility/analytics path. Value-bearing execution must +//! // use generate_certified offline, pin the emitted certificate ID in trusted +//! // program configuration, and call certified()/eval_certified() on-chain. //! # let _ = result; //! # } //! # Ok(()) //! # } //! ``` +use crate::arithmetic::fp_sqrt; use crate::error::SolMathError; use crate::SCALE_I; const N: usize = 64; +const DENSE_N: usize = 129; const S6: i64 = 1_000_000; const SHIFT: i64 = 1_000_000; const DOMAIN_MIN: i64 = -4_000_000_000_000; const DOMAIN_MAX: i64 = 4_000_000_000_000; const RANGE: i64 = 8_000_000_000_000; -const N_MINUS_1: i64 = 63; - -const WN: usize = 1024; -const WS: u32 = 30; -const FRAC_DIVISOR: i64 = RANGE / WN as i64; - -/// Precomputed Catmull-Rom basis weights at 2^30. -const CR_W: [[i32; 4]; WN] = { - let mut out = [[0i32; 4]; WN]; - let s: i64 = 1 << WS; - let mut k = 0usize; - while k < WN { - let t = (k as i64) << (WS - 10); - let t2 = t * t >> WS; - let t3 = t2 * t >> WS; - out[k][0] = ((-t + 2 * t2 - t3) / 2) as i32; - out[k][1] = ((2 * s - 5 * t2 + 3 * t3) / 2) as i32; - out[k][2] = ((t + 4 * t2 - 3 * t3) / 2) as i32; - out[k][3] = ((-t2 + t3) / 2) as i32; - k += 1; - } - out -}; - -#[inline(always)] -fn cr_dot(w: &[i32; 4], p0: i64, p1: i64, p2: i64, p3: i64) -> i64 { - (w[0] as i64 * p0 + w[1] as i64 * p1 + w[2] as i64 * p2 + w[3] as i64 * p3) >> WS +const CERTIFICATE_VERSION: u16 = 1; +const BILINEAR_INTERPOLATION_ID: u8 = 1; +const BVN_GL20_REFERENCE_ID: u8 = 1; +const MAX_CERTIFIED_RHO: i128 = 990_000_000_000; +/// Bytes retained from SHA-256 for each authenticated table-row commitment. +pub const PHI2_ROW_DIGEST_BYTES: usize = 16; +const ROW_DIGEST_BYTES: usize = PHI2_ROW_DIGEST_BYTES; + +// ceil(1 / sqrt(2*pi*e) * SCALE) and ceil(1 / (2*pi) * SCALE). +// These bound |x*phi(x)| and phi(x)*phi(y), respectively, and are used in +// the analytic second-derivative bound for bilinear interpolation. +const MAX_X_PHI: i128 = 241_970_724_520; +const INV_TWO_PI_CEIL: i128 = 159_154_943_092; +// Two staged round-to-nearest interpolations contribute at most one stored +// table unit in total (1e-6 probability). +const BILINEAR_EVALUATION_ROUNDING_ERROR: i128 = SHIFT as i128; + +// The GL20 reference implementation's fresh external-reference corpus +// observed 123 raw SCALE units at |rho| <= .99. Certification deliberately +// reserves a much larger 1e-6 probability allowance. This is an explicit +// assumption in every certificate, not a claim of formal verification of +// the GL20 implementation itself. +const GL20_REFERENCE_ABS_ERROR_ALLOWANCE: i128 = 1_000_000; + +/// Number of points on each axis in the compatibility lookup table. +pub const PHI2_GRID_SIZE: usize = N; + +/// Number of points on each axis in [`Phi2DenseTable`]. +pub const PHI2_DENSE_GRID_SIZE: usize = DENSE_N; + +/// The interpolation algorithm covered by a [`Phi2Certificate`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum Phi2Interpolation { + /// Convex, non-overshooting bilinear interpolation. + Bilinear = BILINEAR_INTERPOLATION_ID, +} + +/// The offline reference covered by a [`Phi2Certificate`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum Phi2Reference { + /// `bvn_cdf_hp` (GL20), plus the certificate's explicit error allowance. + BvnGl20 = BVN_GL20_REFERENCE_ID, +} + +/// Precision metadata for one exact table, correlation, grid, and +/// interpolation algorithm. +/// +/// Certificates are created by exhaustive offline node comparison through +/// [`Phi2Table::certify`] or [`Phi2DenseTable::certify`]. The certificate ID +/// is SHA-256 over all metadata and a SHA-256 root of 128-bit SHA-256 row +/// commitments. On-chain callers must pin an independently trusted +/// `certificate_id` (normally embedded in the program) when creating a +/// [`CertifiedPhi2Evaluator`]. This prevents an untrusted account from lowering +/// the declared error and recomputing a new certificate for itself. +/// +/// `max_abs_error` is measured in the crate's `SCALE` (1e12). It is the sum +/// of the largest generated-node discrepancy from GL20, the declared GL20 +/// reference allowance, and a conservative analytic continuous-cell +/// bilinear interpolation bound including fixed-point evaluation rounding. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Phi2Certificate { + version: u16, + grid_size: u16, + interpolation_id: u8, + reference_id: u8, + rho: i128, + domain_min: i128, + domain_max: i128, + value_scale: i128, + max_node_abs_error: i128, + interpolation_abs_error_bound: i128, + reference_abs_error_allowance: i128, + max_abs_error: i128, + row_digests: [[u8; ROW_DIGEST_BYTES]; DENSE_N], + table_digest: [u8; 32], + certificate_id: [u8; 32], +} + +impl Phi2Certificate { + /// Reconstruct certificate metadata emitted by trusted offline tooling. + /// + /// This constructor intentionally does not establish trust. Evaluation + /// still requires the independently pinned `expected_certificate_id`, and + /// verifies the ID, all derived bounds, and the complete table digest. + #[allow(clippy::too_many_arguments)] + pub const fn from_embedded_parts( + rho: i128, + grid_size: u16, + max_node_abs_error: i128, + interpolation_abs_error_bound: i128, + reference_abs_error_allowance: i128, + max_abs_error: i128, + row_digests: [[u8; PHI2_ROW_DIGEST_BYTES]; PHI2_DENSE_GRID_SIZE], + table_digest: [u8; 32], + certificate_id: [u8; 32], + ) -> Self { + Self { + version: CERTIFICATE_VERSION, + grid_size, + interpolation_id: BILINEAR_INTERPOLATION_ID, + reference_id: BVN_GL20_REFERENCE_ID, + rho, + domain_min: DOMAIN_MIN as i128, + domain_max: DOMAIN_MAX as i128, + value_scale: S6 as i128, + max_node_abs_error, + interpolation_abs_error_bound, + reference_abs_error_allowance, + max_abs_error, + row_digests, + table_digest, + certificate_id, + } + } + + /// Fixed correlation at `SCALE` (1e12). + pub const fn rho(&self) -> i128 { + self.rho + } + + /// Number of points on each grid axis. + pub const fn grid_size(&self) -> usize { + self.grid_size as usize + } + + /// Interpolation algorithm committed by the certificate. + pub const fn interpolation(&self) -> Phi2Interpolation { + Phi2Interpolation::Bilinear + } + + /// Offline numerical reference committed by the certificate. + pub const fn reference(&self) -> Phi2Reference { + Phi2Reference::BvnGl20 + } + + /// Maximum table-node discrepancy from the offline GL20 reference. + pub const fn max_node_abs_error(&self) -> i128 { + self.max_node_abs_error + } + + /// Conservative continuous-cell interpolation plus evaluator-rounding bound. + pub const fn interpolation_abs_error_bound(&self) -> i128 { + self.interpolation_abs_error_bound + } + + /// Explicit allowance for error in the GL20 reference itself. + pub const fn reference_abs_error_allowance(&self) -> i128 { + self.reference_abs_error_allowance + } + + /// Total certified maximum absolute probability error at `SCALE` (1e12). + pub const fn max_abs_error(&self) -> i128 { + self.max_abs_error + } + + /// Truncated SHA-256 commitments for each row. + /// + /// Once the certificate ID is independently pinned, changing a committed + /// row requires a 128-bit second-preimage attack. The generic collision + /// strength of a 128-bit truncated digest is 64 bits; do not use these row + /// digests as unpinned, attacker-selected identities. + pub const fn row_digests(&self) -> &[[u8; PHI2_ROW_DIGEST_BYTES]; PHI2_DENSE_GRID_SIZE] { + &self.row_digests + } + + /// SHA-256 digest of the exact table and fixed grid/interpolation metadata. + pub const fn table_digest(&self) -> [u8; 32] { + self.table_digest + } + + /// SHA-256 identity of the table digest plus all certificate metadata. + pub const fn certificate_id(&self) -> [u8; 32] { + self.certificate_id + } +} + +/// An evaluator whose certificate identity and economic error budget have +/// already been checked. +/// +/// Construct through [`Phi2Table::certified`] or +/// [`Phi2DenseTable::certified`]. Each lookup authenticates only the two rows +/// that can affect its result; reuse this guard to avoid rehashing the complete +/// certificate row-root metadata. +pub struct CertifiedPhi2Evaluator<'a, const M: usize> { + values: &'a [[i32; M]; M], + certificate: &'a Phi2Certificate, +} + +impl CertifiedPhi2Evaluator<'_, M> { + /// Evaluate within the certified `[-4, 4]^2` domain. + /// + /// Unlike the compatibility [`Phi2Table::eval`] API, certified evaluation + /// rejects out-of-domain inputs instead of clamping them, because clamping + /// would invalidate the error statement relative to the caller's input. + pub fn eval(&self, a: i128, b: i128) -> Result { + if !(DOMAIN_MIN as i128..=DOMAIN_MAX as i128).contains(&a) + || !(DOMAIN_MIN as i128..=DOMAIN_MAX as i128).contains(&b) + { + return Err(SolMathError::DomainError); + } + let (first_row, _) = cell_index::(a as i64)?; + authenticate_rows(self.values, self.certificate, first_row)?; + eval_bilinear(self.values, a, b) + } + + /// Certificate used to establish this guard. + pub const fn certificate(&self) -> &Phi2Certificate { + self.certificate + } +} + +#[inline] +fn div_ceil_nonnegative(numerator: i128, denominator: i128) -> Result { + if numerator < 0 || denominator <= 0 { + return Err(SolMathError::DomainError); + } + let quotient = numerator / denominator; + if numerator % denominator == 0 { + Ok(quotient) + } else { + quotient.checked_add(1).ok_or(SolMathError::Overflow) + } +} + +#[inline] +fn mul_scale_ceil_nonnegative(a: i128, b: i128) -> Result { + let product = a.checked_mul(b).ok_or(SolMathError::Overflow)?; + div_ceil_nonnegative(product, SCALE_I) +} + +#[inline] +fn div_scale_ceil_nonnegative(a: i128, b: i128) -> Result { + let numerator = a.checked_mul(SCALE_I).ok_or(SolMathError::Overflow)?; + div_ceil_nonnegative(numerator, b) +} + +/// Conservative global error of the fixed-point bilinear evaluator relative +/// to the mathematical bivariate normal CDF, assuming bounded node errors. +/// +/// For each axis, +/// `|F_xx| <= max |x phi(x)| + |rho|/sqrt(1-rho^2) max phi(x)phi(z)`. +/// Tensor-product linear interpolation then contributes at most +/// `h^2/8 * sup|F_xx|` per axis. All fixed-point operations round upward. +fn interpolation_error_bound(rho: i128, cells: usize) -> Result { + if cells == 0 || rho.unsigned_abs() > MAX_CERTIFIED_RHO as u128 { + return Err(SolMathError::DomainError); + } + let rho_abs = rho.unsigned_abs() as i128; + let rho_sq = mul_scale_ceil_nonnegative(rho_abs, rho_abs)?; + let conditional_variance = SCALE_I + .checked_sub(rho_sq) + .ok_or(SolMathError::DomainError)?; + let conditional_std = fp_sqrt(conditional_variance as u128)? as i128; + if conditional_std == 0 { + return Err(SolMathError::DomainError); + } + let rho_over_std = div_scale_ceil_nonnegative(rho_abs, conditional_std)?; + let correlation_term = mul_scale_ceil_nonnegative(INV_TWO_PI_CEIL, rho_over_std)?; + let second_derivative_bound = MAX_X_PHI + .checked_add(correlation_term) + .ok_or(SolMathError::Overflow)?; + + let spacing = div_ceil_nonnegative(RANGE as i128, cells as i128)?; + let spacing_sq = mul_scale_ceil_nonnegative(spacing, spacing)?; + let both_axes = mul_scale_ceil_nonnegative(spacing_sq, second_derivative_bound)?; + div_ceil_nonnegative(both_axes, 4)? + .checked_add(BILINEAR_EVALUATION_ROUNDING_ERROR) + .ok_or(SolMathError::Overflow) +} + +#[inline] +fn lerp_grid(a: i32, b: i32, fraction: i64) -> i128 { + let f = fraction as i128; + let range = RANGE as i128; + (a as i128 * (range - f) + b as i128 * f + range / 2) / range +} + +fn cell_index(coordinate: i64) -> Result<(usize, i64), SolMathError> { + if M < 2 || M - 1 > i64::MAX as usize { + return Err(SolMathError::DomainError); + } + let cells = (M - 1) as i64; + let offset = coordinate + .checked_sub(DOMAIN_MIN) + .ok_or(SolMathError::Overflow)?; + let scaled = offset.checked_mul(cells).ok_or(SolMathError::Overflow)?; + let index = ((scaled / RANGE) as usize).min(M - 2); + Ok((index, scaled - index as i64 * RANGE)) +} + +fn eval_bilinear( + values: &[[i32; M]; M], + a: i128, + b: i128, +) -> Result { + let a64 = (a.clamp(DOMAIN_MIN as i128, DOMAIN_MAX as i128)) as i64; + let b64 = (b.clamp(DOMAIN_MIN as i128, DOMAIN_MAX as i128)) as i64; + let (i0, fa) = cell_index::(a64)?; + let (j0, fb) = cell_index::(b64)?; + let low = lerp_grid(values[i0][j0], values[i0][j0 + 1], fb); + let high = lerp_grid(values[i0 + 1][j0], values[i0 + 1][j0 + 1], fb); + let result_s6 = (low * (RANGE as i128 - fa as i128) + high * fa as i128 + RANGE as i128 / 2) + / RANGE as i128; + Ok(result_s6.clamp(0, S6 as i128) * SHIFT as i128) +} + +// Minimal streaming SHA-256. The implementation uses fixed-size stack data, +// no allocation, and no unsafe code, so certificate verification is available +// in no_std/SBF builds. A guard verifies the small row-commitment root once; +// each evaluation then hashes only the two rows that affect its result. +struct Sha256 { + state: [u32; 8], + block: [u8; 64], + block_len: usize, + total_len: u64, +} + +impl Sha256 { + const fn new() -> Self { + Self { + state: [ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, + 0x5be0cd19, + ], + block: [0; 64], + block_len: 0, + total_len: 0, + } + } + + fn update(&mut self, bytes: &[u8]) { + for &byte in bytes { + self.block[self.block_len] = byte; + self.block_len += 1; + self.total_len = self.total_len.wrapping_add(1); + if self.block_len == 64 { + let block = self.block; + self.compress(&block); + self.block_len = 0; + } + } + } + + fn finish(mut self) -> [u8; 32] { + let bit_len = self.total_len.wrapping_mul(8); + self.block[self.block_len] = 0x80; + self.block_len += 1; + if self.block_len > 56 { + for byte in &mut self.block[self.block_len..] { + *byte = 0; + } + let block = self.block; + self.compress(&block); + self.block = [0; 64]; + self.block_len = 0; + } + for byte in &mut self.block[self.block_len..56] { + *byte = 0; + } + self.block[56..64].copy_from_slice(&bit_len.to_be_bytes()); + let block = self.block; + self.compress(&block); + + let mut digest = [0u8; 32]; + for (index, word) in self.state.iter().enumerate() { + digest[index * 4..index * 4 + 4].copy_from_slice(&word.to_be_bytes()); + } + digest + } + + fn compress(&mut self, block: &[u8; 64]) { + const K: [u32; 64] = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, + 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, + 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, + 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, + 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, + 0xc67178f2, + ]; + + let mut words = [0u32; 64]; + for (index, word) in words.iter_mut().enumerate().take(16) { + let offset = index * 4; + *word = u32::from_be_bytes([ + block[offset], + block[offset + 1], + block[offset + 2], + block[offset + 3], + ]); + } + for index in 16..64 { + let s0 = words[index - 15].rotate_right(7) + ^ words[index - 15].rotate_right(18) + ^ (words[index - 15] >> 3); + let s1 = words[index - 2].rotate_right(17) + ^ words[index - 2].rotate_right(19) + ^ (words[index - 2] >> 10); + words[index] = words[index - 16] + .wrapping_add(s0) + .wrapping_add(words[index - 7]) + .wrapping_add(s1); + } + + let mut a = self.state[0]; + let mut b = self.state[1]; + let mut c = self.state[2]; + let mut d = self.state[3]; + let mut e = self.state[4]; + let mut f = self.state[5]; + let mut g = self.state[6]; + let mut h = self.state[7]; + for index in 0..64 { + let sigma1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25); + let choose = (e & f) ^ ((!e) & g); + let temp1 = h + .wrapping_add(sigma1) + .wrapping_add(choose) + .wrapping_add(K[index]) + .wrapping_add(words[index]); + let sigma0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22); + let majority = (a & b) ^ (a & c) ^ (b & c); + let temp2 = sigma0.wrapping_add(majority); + h = g; + g = f; + f = e; + e = d.wrapping_add(temp1); + d = c; + c = b; + b = a; + a = temp1.wrapping_add(temp2); + } + + self.state[0] = self.state[0].wrapping_add(a); + self.state[1] = self.state[1].wrapping_add(b); + self.state[2] = self.state[2].wrapping_add(c); + self.state[3] = self.state[3].wrapping_add(d); + self.state[4] = self.state[4].wrapping_add(e); + self.state[5] = self.state[5].wrapping_add(f); + self.state[6] = self.state[6].wrapping_add(g); + self.state[7] = self.state[7].wrapping_add(h); + } +} + +fn row_digest(row: &[i32; M], row_index: usize) -> [u8; ROW_DIGEST_BYTES] { + let mut hash = Sha256::new(); + hash.update(b"solmath.phi2table.row.v1"); + hash.update(&(M as u64).to_le_bytes()); + hash.update(&(row_index as u64).to_le_bytes()); + hash.update(&(DOMAIN_MIN as i128).to_le_bytes()); + hash.update(&(DOMAIN_MAX as i128).to_le_bytes()); + hash.update(&(S6 as i128).to_le_bytes()); + hash.update(&[BILINEAR_INTERPOLATION_ID]); + for value in row { + hash.update(&value.to_le_bytes()); + } + let full = hash.finish(); + let mut truncated = [0u8; ROW_DIGEST_BYTES]; + truncated.copy_from_slice(&full[..ROW_DIGEST_BYTES]); + truncated +} + +fn table_digest_from_rows( + grid_size: usize, + row_digests: &[[u8; ROW_DIGEST_BYTES]; DENSE_N], +) -> [u8; 32] { + let mut hash = Sha256::new(); + hash.update(b"solmath.phi2table.rows.v1"); + hash.update(&(grid_size as u64).to_le_bytes()); + hash.update(&(DOMAIN_MIN as i128).to_le_bytes()); + hash.update(&(DOMAIN_MAX as i128).to_le_bytes()); + hash.update(&(S6 as i128).to_le_bytes()); + hash.update(&[BILINEAR_INTERPOLATION_ID]); + for digest in row_digests { + hash.update(digest); + } + hash.finish() +} + +#[cfg(feature = "table-gen")] +fn table_commitment( + values: &[[i32; M]; M], +) -> ([[u8; ROW_DIGEST_BYTES]; DENSE_N], [u8; 32]) { + let mut rows = [[0u8; ROW_DIGEST_BYTES]; DENSE_N]; + for index in 0..M { + rows[index] = row_digest(&values[index], index); + } + let root = table_digest_from_rows(M, &rows); + (rows, root) +} + +fn certificate_digest(certificate: &Phi2Certificate) -> [u8; 32] { + let mut hash = Sha256::new(); + hash.update(b"solmath.phi2table.certificate.v1"); + hash.update(&certificate.version.to_le_bytes()); + hash.update(&certificate.grid_size.to_le_bytes()); + hash.update(&[certificate.interpolation_id, certificate.reference_id]); + hash.update(&certificate.rho.to_le_bytes()); + hash.update(&certificate.domain_min.to_le_bytes()); + hash.update(&certificate.domain_max.to_le_bytes()); + hash.update(&certificate.value_scale.to_le_bytes()); + hash.update(&certificate.max_node_abs_error.to_le_bytes()); + hash.update(&certificate.interpolation_abs_error_bound.to_le_bytes()); + hash.update(&certificate.reference_abs_error_allowance.to_le_bytes()); + hash.update(&certificate.max_abs_error.to_le_bytes()); + hash.update(&certificate.table_digest); + hash.finish() +} + +fn authenticate_rows( + values: &[[i32; M]; M], + certificate: &Phi2Certificate, + first_row: usize, +) -> Result<(), SolMathError> { + if first_row + 1 >= M { + return Err(SolMathError::DomainError); + } + for row_index in first_row..=first_row + 1 { + let row = &values[row_index]; + if row_digest(row, row_index) != certificate.row_digests[row_index] { + return Err(SolMathError::DomainError); + } + for column in 0..M { + if !(0..=S6 as i32).contains(&row[column]) + || (column > 0 && row[column] < row[column - 1]) + || (row_index > first_row && row[column] < values[row_index - 1][column]) + { + return Err(SolMathError::DomainError); + } + } + } + Ok(()) +} + +#[cfg(feature = "table-gen")] +fn validate_grid(values: &[[i32; M]; M]) -> Result<(), SolMathError> { + if M < 2 || M > u16::MAX as usize { + return Err(SolMathError::DomainError); + } + for i in 0..M { + for j in 0..M { + let value = values[i][j]; + if !(0..=S6 as i32).contains(&value) { + return Err(SolMathError::DomainError); + } + if i > 0 && value < values[i - 1][j] { + return Err(SolMathError::DomainError); + } + if j > 0 && value < values[i][j - 1] { + return Err(SolMathError::DomainError); + } + } + } + Ok(()) +} + +fn verify_certificate( + certificate: &Phi2Certificate, + expected_certificate_id: [u8; 32], + max_abs_error_budget: i128, +) -> Result<(), SolMathError> { + if M < 2 || M > DENSE_N || !(0..=SCALE_I).contains(&max_abs_error_budget) { + return Err(SolMathError::DomainError); + } + // Check the caller's trust anchor before considering any untrusted + // certificate claim. + if certificate.certificate_id != expected_certificate_id { + return Err(SolMathError::DomainError); + } + if certificate.version != CERTIFICATE_VERSION + || certificate.grid_size as usize != M + || certificate.interpolation_id != BILINEAR_INTERPOLATION_ID + || certificate.reference_id != BVN_GL20_REFERENCE_ID + || certificate.domain_min != DOMAIN_MIN as i128 + || certificate.domain_max != DOMAIN_MAX as i128 + || certificate.value_scale != S6 as i128 + || certificate.reference_abs_error_allowance != GL20_REFERENCE_ABS_ERROR_ALLOWANCE + || certificate.max_node_abs_error < 0 + { + return Err(SolMathError::DomainError); + } + let interpolation_bound = interpolation_error_bound(certificate.rho, M - 1)?; + if interpolation_bound != certificate.interpolation_abs_error_bound { + return Err(SolMathError::DomainError); + } + let total = certificate + .max_node_abs_error + .checked_add(certificate.interpolation_abs_error_bound) + .and_then(|value| value.checked_add(certificate.reference_abs_error_allowance)) + .ok_or(SolMathError::Overflow)?; + if total != certificate.max_abs_error + || certificate.row_digests[M..] + .iter() + .any(|digest| *digest != [0; ROW_DIGEST_BYTES]) + || table_digest_from_rows(M, &certificate.row_digests) != certificate.table_digest + || certificate_digest(certificate) != certificate.certificate_id + { + return Err(SolMathError::DomainError); + } + if certificate.max_abs_error > max_abs_error_budget { + return Err(SolMathError::NoConvergence); + } + Ok(()) +} + +#[cfg(feature = "table-gen")] +fn grid_coordinate(index: usize, points: usize) -> Result { + if points < 2 || index >= points { + return Err(SolMathError::DomainError); + } + Ok(DOMAIN_MIN as i128 + (RANGE as i128 * index as i128) / (points as i128 - 1)) +} + +#[cfg(feature = "table-gen")] +fn generate_grid(rho: i128) -> Result<[[i32; M]; M], SolMathError> { + use crate::bvn_cdf::bvn_cdf_hp; + + if M < 2 || M > u16::MAX as usize || rho.unsigned_abs() > SCALE_I as u128 { + return Err(SolMathError::DomainError); + } + let mut values = [[0i32; M]; M]; + for (i, row) in values.iter_mut().enumerate() { + let a_fp = grid_coordinate(i, M)?; + for (j, slot) in row.iter_mut().enumerate() { + let b_fp = grid_coordinate(j, M)?; + let value = bvn_cdf_hp(a_fp, b_fp, rho)?; + let rounded = value + .checked_add(SHIFT as i128 / 2) + .ok_or(SolMathError::Overflow)? + / SHIFT as i128; + if !(0..=S6 as i128).contains(&rounded) { + return Err(SolMathError::Overflow); + } + *slot = rounded as i32; + } + } + validate_grid(&values)?; + Ok(values) +} + +#[cfg(feature = "table-gen")] +fn certify_grid( + values: &[[i32; M]; M], + rho: i128, +) -> Result { + use crate::bvn_cdf::bvn_cdf_hp; + + if rho.unsigned_abs() > MAX_CERTIFIED_RHO as u128 { + return Err(SolMathError::DomainError); + } + validate_grid(values)?; + let mut max_node_abs_error = 0i128; + for (i, row) in values.iter().enumerate() { + let a_fp = grid_coordinate(i, M)?; + for (j, stored_value) in row.iter().enumerate() { + let b_fp = grid_coordinate(j, M)?; + let reference = bvn_cdf_hp(a_fp, b_fp, rho)?; + let stored = (*stored_value as i128) + .checked_mul(SHIFT as i128) + .ok_or(SolMathError::Overflow)?; + let error = stored.abs_diff(reference); + if error > i128::MAX as u128 { + return Err(SolMathError::Overflow); + } + max_node_abs_error = max_node_abs_error.max(error as i128); + } + } + let interpolation_abs_error_bound = interpolation_error_bound(rho, M - 1)?; + let max_abs_error = max_node_abs_error + .checked_add(interpolation_abs_error_bound) + .and_then(|value| value.checked_add(GL20_REFERENCE_ABS_ERROR_ALLOWANCE)) + .ok_or(SolMathError::Overflow)?; + let (row_digests, table_digest) = table_commitment(values); + let mut certificate = Phi2Certificate::from_embedded_parts( + rho, + M as u16, + max_node_abs_error, + interpolation_abs_error_bound, + GL20_REFERENCE_ABS_ERROR_ALLOWANCE, + max_abs_error, + row_digests, + table_digest, + [0; 32], + ); + certificate.certificate_id = certificate_digest(&certificate); + Ok(certificate) } /// Precomputed bivariate normal CDF table at a fixed correlation. /// /// Stores Φ₂(a, b; ρ) on a 64×64 grid over `[-4, +4]²` at `SCALE_6` (10⁶) -/// precision. On-chain evaluation via [`Phi2Table::eval`] uses Catmull-Rom -/// bicubic interpolation in pure i64 arithmetic. +/// precision. On-chain evaluation via [`Phi2Table::eval`] uses bilinear +/// interpolation without cubic overshoot. /// /// # Performance /// -/// - **943 CU** per evaluation on SBF (constant, input-independent) -/// - **64 KB** storage per table (embedded as `const` in the binary) +/// - Constant, input-independent evaluation cost +/// - **16 KB** storage per table (embed as `static` read-only program data) /// /// # Accuracy /// -/// Max absolute error < 9.0×10⁻⁵ (validated across 630K vectors). +/// [`Phi2Table::eval`] is the compatibility, uncertified API. Interpolation +/// error depends strongly on rho and cell curvature. Use +/// [`Phi2Table::certified`] with a pinned certificate ID and an explicit +/// economic error budget when the result moves value. /// /// # Construction /// @@ -101,7 +773,16 @@ impl Phi2Table { Phi2Table { values } } - /// Evaluate Φ₂(a, b; ρ) via Catmull-Rom bicubic interpolation. ~943 CU. + /// Borrow the raw compatibility grid for offline code generation/embedding. + pub const fn as_array(&self) -> &[[i32; N]; N] { + &self.values + } + + /// Evaluate Φ₂(a, b; ρ) via non-overshooting bilinear interpolation. + /// + /// This compatibility method is **uncertified**: it does not know the + /// table's rho or validate the contents against a reference. Economic code + /// should use [`Phi2Table::certified`] or [`Phi2Table::eval_certified`]. /// /// All inputs/outputs are signed fixed-point `i128` at `SCALE` (1e12). /// @@ -111,82 +792,181 @@ impl Phi2Table { /// /// # Accuracy /// - /// Max absolute error < 9.0×10⁻⁵. + /// Preserves monotonicity whenever the supplied grid is monotone. /// /// # Errors /// /// Returns `Ok` for all inputs. Cannot fail in practice — the `Result` /// wrapper is for API consistency with [`bvn_cdf`](crate::bvn_cdf()). pub fn eval(&self, a: i128, b: i128) -> Result { - let a64 = (a.clamp(DOMAIN_MIN as i128, DOMAIN_MAX as i128)) as i64; - let b64 = (b.clamp(DOMAIN_MIN as i128, DOMAIN_MAX as i128)) as i64; - - let a_off = a64 - DOMAIN_MIN; - let b_off = b64 - DOMAIN_MIN; - - let ia_scaled = a_off * N_MINUS_1; - let i0 = (ia_scaled / RANGE) as i32; - - let ib_scaled = b_off * N_MINUS_1; - let j0 = (ib_scaled / RANGE) as i32; - - let wi_a = ((ia_scaled % RANGE) / FRAC_DIVISOR) as usize; - let wi_b = ((ib_scaled % RANGE) / FRAC_DIVISOR) as usize; - let wa = &CR_W[if wi_a < WN { wi_a } else { WN - 1 }]; - let wb = &CR_W[if wi_b < WN { wi_b } else { WN - 1 }]; + eval_bilinear(&self.values, a, b) + } - let i0 = i0.min(N as i32 - 2); - let j0 = j0.min(N as i32 - 2); - let n = N as i32; + /// Verify certificate metadata against a pinned identity and caller-supplied + /// economic error budget, returning a row-authenticating evaluator. + /// + /// `expected_certificate_id` must come from trusted program configuration, + /// not from the same untrusted account as `certificate`. Returns + /// `NoConvergence` when the certified bound exceeds the budget, and + /// `DomainError` for invalid, forged, mismatched, or corrupt metadata. + pub fn certified<'a>( + &'a self, + certificate: &'a Phi2Certificate, + expected_certificate_id: [u8; 32], + max_abs_error_budget: i128, + ) -> Result, SolMathError> { + verify_certificate::(certificate, expected_certificate_id, max_abs_error_budget)?; + Ok(CertifiedPhi2Evaluator { + values: &self.values, + certificate, + }) + } - let mut cols = [0i64; 4]; - for di in 0..4i32 { - let ii = (i0 - 1 + di).clamp(0, n - 1) as usize; - let p0 = self.values[ii][(j0 - 1).clamp(0, n - 1) as usize] as i64; - let p1 = self.values[ii][j0.clamp(0, n - 1) as usize] as i64; - let p2 = self.values[ii][(j0 + 1).clamp(0, n - 1) as usize] as i64; - let p3 = self.values[ii][(j0 + 2).clamp(0, n - 1) as usize] as i64; - cols[di as usize] = cr_dot(wb, p0, p1, p2, p3); - } + /// Verify and evaluate once under an explicit error budget. + /// + /// For multiple evaluations, call [`Phi2Table::certified`] once and reuse + /// the returned guard to avoid repeatedly hashing certificate-root metadata. + #[allow(clippy::too_many_arguments)] + pub fn eval_certified( + &self, + a: i128, + b: i128, + certificate: &Phi2Certificate, + expected_certificate_id: [u8; 32], + max_abs_error_budget: i128, + ) -> Result { + self.certified(certificate, expected_certificate_id, max_abs_error_budget)? + .eval(a, b) + } - let result_s6 = cr_dot(wa, cols[0], cols[1], cols[2], cols[3]); - let clamped = result_s6.clamp(0, S6); - Ok(clamped as i128 * SHIFT as i128) + /// Exhaustively compare all stored nodes with GL20 and create a bound + /// certificate for this exact table and rho. + /// + /// Offline only. Certification is available for `|rho| <= 0.99`; the + /// continuous bilinear bound becomes singular as `|rho|` approaches one. + #[cfg(feature = "table-gen")] + pub fn certify(&self, rho: i128) -> Result { + certify_grid(&self.values, rho) } - /// Generate a Phi2Table offline using the high-precision GL20 bvn_cdf. + /// Generate an **uncertified** Phi2Table offline using GL20 `bvn_cdf_hp`. /// /// `rho` is the fixed correlation at `SCALE` (1e12), as `i128`. /// Generates a 64×64 table covering `[-4, +4]²`. /// - /// This is expensive (~331K CU × 4096 entries) and intended to run as a - /// native Rust binary, not on-chain. + /// This is expensive (4096 GL20 evaluations; the final SBF audit observed + /// up to 468,417 CU for one) and intended to run as a native Rust binary, + /// not on-chain. /// /// # Errors /// /// Returns `DomainError` if `n != 64` or `|rho| > SCALE`. #[cfg(feature = "table-gen")] pub fn generate(rho: i128, n: usize) -> Result { - use crate::bvn_cdf::bvn_cdf_hp; - if n != N { return Err(SolMathError::DomainError); } - if rho.abs() > SCALE_I { - return Err(SolMathError::DomainError); - } + Ok(Phi2Table { + values: generate_grid(rho)?, + }) + } - let mut values = [[0i32; N]; N]; - for i in 0..N { - let a_fp = DOMAIN_MIN as i128 + (RANGE as i128 * i as i128) / (N as i128 - 1); - for j in 0..N { - let b_fp = DOMAIN_MIN as i128 + (RANGE as i128 * j as i128) / (N as i128 - 1); - let val = bvn_cdf_hp(a_fp, b_fp, rho)?; - values[i][j] = (val / SHIFT as i128) as i32; - } + /// Generate and then certify a 64×64 table offline. + #[cfg(feature = "table-gen")] + pub fn generate_certified( + rho: i128, + n: usize, + ) -> Result<(Self, Phi2Certificate), SolMathError> { + let table = Self::generate(rho, n)?; + let certificate = table.certify(rho)?; + Ok((table, certificate)) + } +} + +/// Denser fixed-size bivariate normal lookup table for value-sensitive paths. +/// +/// Stores a 129×129 grid (128 cells per axis) at `SCALE_6`, occupying 66,564 +/// bytes. It uses no heap allocation and evaluates with the same constant-cost +/// four-node bilinear interpolation as [`Phi2Table`]. Generation is offline; +/// embed the resulting array as `static` program read-only data rather than +/// constructing or copying it on the SBF stack. +/// +/// At rho=0.75, the analytic interpolation component of the certificate is +/// about 4.14e-4, versus about 1.70e-3 for the compatibility 64×64 table. +pub struct Phi2DenseTable { + values: [[i32; DENSE_N]; DENSE_N], +} + +impl Phi2DenseTable { + /// Create an uncertified dense table from a pre-generated 129×129 array. + pub const fn from_array(values: [[i32; DENSE_N]; DENSE_N]) -> Self { + Self { values } + } + + /// Borrow the raw dense grid for offline code generation/embedding. + pub const fn as_array(&self) -> &[[i32; DENSE_N]; DENSE_N] { + &self.values + } + + /// Uncertified compatibility evaluation. Inputs outside `[-4, 4]²` clamp. + pub fn eval(&self, a: i128, b: i128) -> Result { + eval_bilinear(&self.values, a, b) + } + + /// Verify dense certificate metadata and return a row-authenticating guard. + pub fn certified<'a>( + &'a self, + certificate: &'a Phi2Certificate, + expected_certificate_id: [u8; 32], + max_abs_error_budget: i128, + ) -> Result, SolMathError> { + verify_certificate::(certificate, expected_certificate_id, max_abs_error_budget)?; + Ok(CertifiedPhi2Evaluator { + values: &self.values, + certificate, + }) + } + + /// Verify and evaluate the dense table once under an explicit error budget. + #[allow(clippy::too_many_arguments)] + pub fn eval_certified( + &self, + a: i128, + b: i128, + certificate: &Phi2Certificate, + expected_certificate_id: [u8; 32], + max_abs_error_budget: i128, + ) -> Result { + self.certified(certificate, expected_certificate_id, max_abs_error_budget)? + .eval(a, b) + } + + /// Exhaustively certify this exact dense table against GL20 at every node. + #[cfg(feature = "table-gen")] + pub fn certify(&self, rho: i128) -> Result { + certify_grid(&self.values, rho) + } + + /// Generate an uncertified 129×129 table offline. + #[cfg(feature = "table-gen")] + pub fn generate(rho: i128, n: usize) -> Result { + if n != DENSE_N { + return Err(SolMathError::DomainError); } + Ok(Self { + values: generate_grid(rho)?, + }) + } - Ok(Phi2Table { values }) + /// Generate and then certify a 129×129 table offline. + #[cfg(feature = "table-gen")] + pub fn generate_certified( + rho: i128, + n: usize, + ) -> Result<(Self, Phi2Certificate), SolMathError> { + let table = Self::generate(rho, n)?; + let certificate = table.certify(rho)?; + Ok((table, certificate)) } } @@ -194,6 +974,121 @@ impl Phi2Table { mod tests { use super::*; + #[test] + fn certificate_hash_uses_standard_sha256() { + let mut hash = Sha256::new(); + hash.update(b"abc"); + assert_eq!( + hash.finish(), + [ + 0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea, 0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, + 0x22, 0x23, 0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c, 0xb4, 0x10, 0xff, 0x61, + 0xf2, 0x00, 0x15, 0xad, + ] + ); + // NIST's two-block vector exercises padding across a block boundary, + // which is also the path used by row commitments. + let mut long_hash = Sha256::new(); + long_hash.update(b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"); + assert_eq!( + long_hash.finish(), + [ + 0x24, 0x8d, 0x6a, 0x61, 0xd2, 0x06, 0x38, 0xb8, 0xe5, 0xc0, 0x26, 0x93, 0x0c, 0x3e, + 0x60, 0x39, 0xa3, 0x3c, 0xe4, 0x59, 0x64, 0xff, 0x21, 0x67, 0xf6, 0xec, 0xed, 0xd4, + 0x19, 0xdb, 0x06, 0xc1, + ] + ); + // Cross-language serialization vector produced independently with + // Python's hashlib for a 129-element all-zero row at index zero. + assert_eq!( + row_digest(&[0i32; DENSE_N], 0), + [ + 0xe2, 0x1b, 0xe0, 0x59, 0x4e, 0xf0, 0xf6, 0x24, 0x05, 0xc5, 0x35, 0x04, 0x38, 0x1a, + 0x7e, 0x6b, + ] + ); + assert!(core::mem::size_of::() < 3_000); + assert_eq!( + core::mem::size_of::(), + DENSE_N * DENSE_N * core::mem::size_of::() + ); + } + + #[cfg(feature = "table-gen")] + const TEST_RHO: i128 = 750_000_000_000; + + #[cfg(feature = "table-gen")] + fn dense_test_table() -> &'static (Phi2DenseTable, Phi2Certificate) { + use std::sync::OnceLock; + static TABLE: OnceLock<(Phi2DenseTable, Phi2Certificate)> = OnceLock::new(); + TABLE.get_or_init(|| { + Phi2DenseTable::generate_certified(TEST_RHO, DENSE_N) + .expect("dense test table generation") + }) + } + + #[cfg(feature = "table-gen")] + fn compatibility_test_table() -> &'static (Phi2Table, Phi2Certificate) { + use std::sync::OnceLock; + static TABLE: OnceLock<(Phi2Table, Phi2Certificate)> = OnceLock::new(); + TABLE.get_or_init(|| { + Phi2Table::generate_certified(TEST_RHO, N).expect("compatibility test table generation") + }) + } + + // Independent test-only reference: Phi(a)Phi(b) plus numerical Simpson + // integration of Plackett's d Phi2 / d rho identity. It shares no fixed- + // point arithmetic, quadrature nodes, or quadrant folding with bvn_cdf_hp. + #[cfg(feature = "table-gen")] + fn normal_cdf_reference(x: f64) -> f64 { + let z = x / core::f64::consts::SQRT_2; + let t = 1.0 / (1.0 + 0.5 * z.abs()); + let tau = t + * (-z * z - 1.265_512_23 + + t * (1.000_023_68 + + t * (0.374_091_96 + + t * (0.096_784_18 + + t * (-0.186_288_06 + + t * (0.278_868_07 + + t * (-1.135_203_98 + + t * (1.488_515_87 + + t * (-0.822_152_23 + t * 0.170_872_77))))))))) + .exp(); + let erf = if z >= 0.0 { 1.0 - tau } else { tau - 1.0 }; + 0.5 * (1.0 + erf) + } + + #[cfg(feature = "table-gen")] + fn bvn_independent_reference(a: f64, b: f64, rho: f64) -> f64 { + const PANELS: usize = 512; + let base = normal_cdf_reference(a) * normal_cdf_reference(b); + if rho == 0.0 { + return base; + } + let integrand = |r: f64| { + let one_minus_r2 = 1.0 - r * r; + let exponent = -(a * a - 2.0 * r * a * b + b * b) / (2.0 * one_minus_r2); + exponent.exp() / (2.0 * core::f64::consts::PI * one_minus_r2.sqrt()) + }; + let step = rho / PANELS as f64; + let mut weighted = integrand(0.0) + integrand(rho); + for index in 1..PANELS { + let weight = if index % 2 == 0 { 2.0 } else { 4.0 }; + weighted += weight * integrand(index as f64 * step); + } + (base + step * weighted / 3.0).clamp(0.0, 1.0) + } + + #[cfg(feature = "table-gen")] + fn reference_raw(a: i128, b: i128, rho: i128) -> i128 { + let value = bvn_independent_reference( + a as f64 / SCALE_I as f64, + b as f64 / SCALE_I as f64, + rho as f64 / SCALE_I as f64, + ); + (value * SCALE_I as f64).round() as i128 + } + /// Smoke test: generate a table and eval at the origin. #[cfg(feature = "table-gen")] #[test] @@ -204,6 +1099,21 @@ mod tests { assert!(v > 300_000_000_000 && v < 370_000_000_000, "v={v}"); } + /// Regression M10: the top edge snapped its interpolation fraction back + /// to 0, making eval(4, ·) < eval(3.9, ·). + #[cfg(feature = "table-gen")] + #[test] + fn phi2table_edge_monotone() { + let s: i128 = 1_000_000_000_000; + let table = Phi2Table::generate(0, 64).unwrap(); + let near = table.eval(3_900_000_000_000, 0).unwrap(); + let edge = table.eval(4 * s, 0).unwrap(); + assert!(edge >= near, "edge {} < near {}", edge, near); + let near_b = table.eval(0, 3_900_000_000_000).unwrap(); + let edge_b = table.eval(0, 4 * s).unwrap(); + assert!(edge_b >= near_b, "b-axis edge {} < near {}", edge_b, near_b); + } + /// Boundary: far negative → near 0, far positive → near 1. #[cfg(feature = "table-gen")] #[test] @@ -215,4 +1125,265 @@ mod tests { let hi = table.eval(4 * s, 4 * s).unwrap(); assert!(hi > s - s / 1_000, "hi={hi}"); } + + #[cfg(feature = "table-gen")] + #[test] + fn dense_certificate_meets_budget_that_compatibility_grid_rejects() { + let (dense, dense_certificate) = dense_test_table(); + let (compatibility, compatibility_certificate) = compatibility_test_table(); + let economic_budget = 500_000_000i128; // 5e-4 probability. + + assert!(dense_certificate.max_abs_error() <= economic_budget); + assert!(compatibility_certificate.max_abs_error() > economic_budget); + assert!(dense + .certified( + dense_certificate, + dense_certificate.certificate_id(), + economic_budget, + ) + .is_ok()); + assert!(matches!( + compatibility.certified( + compatibility_certificate, + compatibility_certificate.certificate_id(), + economic_budget, + ), + Err(SolMathError::NoConvergence) + )); + + assert!(matches!( + dense.certified( + dense_certificate, + dense_certificate.certificate_id(), + dense_certificate.max_abs_error() - 1, + ), + Err(SolMathError::NoConvergence) + )); + assert!(matches!( + dense.certified(dense_certificate, dense_certificate.certificate_id(), -1), + Err(SolMathError::DomainError) + )); + } + + #[cfg(feature = "table-gen")] + #[test] + fn certificate_rejects_mismatch_corruption_and_recomputed_forgery() { + let (table, certificate) = dense_test_table(); + let trusted_id = certificate.certificate_id(); + let budget = certificate.max_abs_error(); + + let (_, wrong_grid_certificate) = compatibility_test_table(); + assert!(matches!( + table.certified( + wrong_grid_certificate, + wrong_grid_certificate.certificate_id(), + SCALE_I, + ), + Err(SolMathError::DomainError) + )); + + let mut changed_values = table.values; + changed_values[64][64] -= 1; + let changed_table = Phi2DenseTable::from_array(changed_values); + let changed_evaluator = changed_table + .certified(certificate, trusted_id, budget) + .expect("certificate metadata remains valid until the touched row is used"); + assert!(matches!( + changed_evaluator.eval(0, 0), + Err(SolMathError::DomainError) + )); + + let mut corrupt = certificate.clone(); + corrupt.table_digest[0] ^= 1; + assert!(matches!( + table.certified(&corrupt, trusted_id, budget), + Err(SolMathError::DomainError) + )); + + // Model an attacker who lowers the node error and recomputes every + // unkeyed digest. The independently pinned original ID still rejects. + let mut forged = certificate.clone(); + forged.max_node_abs_error = 0; + forged.max_abs_error = + forged.interpolation_abs_error_bound + forged.reference_abs_error_allowance; + forged.certificate_id = certificate_digest(&forged); + assert_ne!(forged.certificate_id(), trusted_id); + assert!(matches!( + table.certified(&forged, trusted_id, budget), + Err(SolMathError::DomainError) + )); + + // Trusted offline parts remain embeddable without weakening checks. + let embedded = Phi2Certificate::from_embedded_parts( + certificate.rho(), + certificate.grid_size() as u16, + certificate.max_node_abs_error(), + certificate.interpolation_abs_error_bound(), + certificate.reference_abs_error_allowance(), + certificate.max_abs_error(), + *certificate.row_digests(), + certificate.table_digest(), + certificate.certificate_id(), + ); + assert!(table.certified(&embedded, trusted_id, budget).is_ok()); + } + + #[cfg(feature = "table-gen")] + #[test] + fn certified_dense_eval_is_monotone_symmetric_and_domain_strict() { + let (table, certificate) = dense_test_table(); + let evaluator = table + .certified( + certificate, + certificate.certificate_id(), + certificate.max_abs_error(), + ) + .unwrap(); + let mut previous_rows = [0i128; 33]; + for i in 0..33 { + let a = DOMAIN_MIN as i128 + RANGE as i128 * i as i128 / 32; + let mut previous_in_row = 0i128; + for j in 0..33 { + let b = DOMAIN_MIN as i128 + RANGE as i128 * j as i128 / 32; + let value = evaluator.eval(a, b).unwrap(); + assert!((0..=SCALE_I).contains(&value)); + if j > 0 { + assert!(value >= previous_in_row, "row monotonicity at {i},{j}"); + } + if i > 0 { + assert!(value >= previous_rows[j], "column monotonicity at {i},{j}"); + } + assert_eq!(value, evaluator.eval(b, a).unwrap()); + previous_in_row = value; + previous_rows[j] = value; + } + } + + for &(a, b) in &[ + (DOMAIN_MIN as i128, DOMAIN_MIN as i128), + (DOMAIN_MIN as i128, DOMAIN_MAX as i128), + (DOMAIN_MAX as i128, DOMAIN_MIN as i128), + (DOMAIN_MAX as i128, DOMAIN_MAX as i128), + ] { + let actual = evaluator.eval(a, b).unwrap(); + let reference = reference_raw(a, b, TEST_RHO); + assert!( + actual.abs_diff(reference) <= certificate.max_abs_error() as u128, + "endpoint ({a},{b}) actual={actual} reference={reference} bound={}", + certificate.max_abs_error() + ); + } + assert_eq!( + evaluator.eval(DOMAIN_MIN as i128 - 1, 0), + Err(SolMathError::DomainError) + ); + assert_eq!( + evaluator.eval(0, DOMAIN_MAX as i128 + 1), + Err(SolMathError::DomainError) + ); + } + + #[cfg(feature = "table-gen")] + #[test] + fn independent_reference_measured_error_is_within_certificate() { + let (dense, dense_certificate) = dense_test_table(); + let (compatibility, compatibility_certificate) = compatibility_test_table(); + let dense_eval = dense + .certified( + dense_certificate, + dense_certificate.certificate_id(), + dense_certificate.max_abs_error(), + ) + .unwrap(); + let compatibility_eval = compatibility + .certified( + compatibility_certificate, + compatibility_certificate.certificate_id(), + compatibility_certificate.max_abs_error(), + ) + .unwrap(); + let mut dense_max = 0u128; + let mut compatibility_max = 0u128; + let mut worst = (0i128, 0i128); + let mut dense_errors = std::vec::Vec::with_capacity(41 * 41); + let mut compatibility_errors = std::vec::Vec::with_capacity(41 * 41); + + // Off-grid deterministic lattice, avoiding a validation corpus made + // only of the same nodes used during certificate generation. + for i in 0..41 { + let a = DOMAIN_MIN as i128 + ((RANGE as i128 * (2 * i + 1) as i128) / 82); + for j in 0..41 { + let b = DOMAIN_MIN as i128 + ((RANGE as i128 * (2 * j + 1) as i128) / 82); + let reference = reference_raw(a, b, TEST_RHO); + let dense_error = dense_eval.eval(a, b).unwrap().abs_diff(reference); + let compatibility_error = + compatibility_eval.eval(a, b).unwrap().abs_diff(reference); + if dense_error > dense_max { + dense_max = dense_error; + worst = (a, b); + } + compatibility_max = compatibility_max.max(compatibility_error); + dense_errors.push(dense_error); + compatibility_errors.push(compatibility_error); + } + } + + dense_errors.sort_unstable(); + compatibility_errors.sort_unstable(); + let median_index = dense_errors.len() / 2; + let p99_index = (dense_errors.len() - 1) * 99 / 100; + let dense_median = dense_errors[median_index]; + let dense_p99 = dense_errors[p99_index]; + let compatibility_median = compatibility_errors[median_index]; + let compatibility_p99 = compatibility_errors[p99_index]; + + std::println!( + "Phi2 rho=.75 independent 41x41 off-grid: dense median={} p99={} max={} ({:.9e}); 64x64 median={} p99={} max={} ({:.9e}); dense_cert={} = node {} + interpolation {} + GL20 allowance {}; 64x64_cert={} = node {} + interpolation {} + GL20 allowance {}; worst=({},{})", + dense_median, + dense_p99, + dense_max, + dense_max as f64 / SCALE_I as f64, + compatibility_median, + compatibility_p99, + compatibility_max, + compatibility_max as f64 / SCALE_I as f64, + dense_certificate.max_abs_error(), + dense_certificate.max_node_abs_error(), + dense_certificate.interpolation_abs_error_bound(), + dense_certificate.reference_abs_error_allowance(), + compatibility_certificate.max_abs_error(), + compatibility_certificate.max_node_abs_error(), + compatibility_certificate.interpolation_abs_error_bound(), + compatibility_certificate.reference_abs_error_allowance(), + worst.0, + worst.1, + ); + assert!(dense_max <= dense_certificate.max_abs_error() as u128); + assert!(compatibility_max <= compatibility_certificate.max_abs_error() as u128); + assert!(dense_max < compatibility_max / 2); + + // At the origin an independent closed form is available: + // Phi2(0,0;rho) = 1/4 + asin(rho)/(2*pi). + let expected_origin = ((0.25 + 0.75f64.asin() / (2.0 * core::f64::consts::PI)) + * SCALE_I as f64) + .round() as i128; + assert!( + dense_eval.eval(0, 0).unwrap().abs_diff(expected_origin) <= SHIFT as u128, + "origin table={} reference={expected_origin}", + dense_eval.eval(0, 0).unwrap() + ); + } + + #[cfg(feature = "table-gen")] + #[test] + fn certification_rejects_bad_grid_and_singular_rho() { + let mut values = [[0i32; N]; N]; + values[0][0] = 1; + let non_monotone = Phi2Table::from_array(values); + assert_eq!(non_monotone.certify(0), Err(SolMathError::DomainError)); + assert!(matches!( + compatibility_test_table().0.certify(MAX_CERTIFIED_RHO + 1), + Err(SolMathError::DomainError) + )); + } } diff --git a/src/pool.rs b/src/pool.rs index 873003c..20081e7 100644 --- a/src/pool.rs +++ b/src/pool.rs @@ -1,8 +1,8 @@ +use crate::arithmetic::{fp_div, fp_div_ceil, fp_mul}; use crate::constants::SCALE; use crate::error::SolMathError; -use crate::arithmetic::{fp_mul, fp_div}; -use crate::mul_div::mul_div_ceil_u128; use crate::hp::pow_fixed_hp; +use crate::mul_div::mul_div_ceil_u128; /// Convert raw token amount (lamports/smallest unit) to fixed-point at SCALE (1e12). /// @@ -24,7 +24,9 @@ pub fn token_to_fp(raw_amount: u64, token_decimals: u8) -> Result Result u64::MAX as u128 { return Err(SolMathError::Overflow); @@ -80,13 +84,18 @@ pub fn fp_to_token_ceil(fp_amount: u128, token_decimals: u8) -> Result u64::MAX as u128 { return Err(SolMathError::Overflow); } return Ok(raw as u64); }; - let raw = fp_amount.checked_add(divisor - 1).ok_or(SolMathError::Overflow)? / divisor; + let raw = fp_amount + .checked_add(divisor - 1) + .ok_or(SolMathError::Overflow)? + / divisor; if raw > u64::MAX as u128 { return Err(SolMathError::Overflow); } @@ -112,9 +121,13 @@ pub fn fp_to_token_ceil(fp_amount: u128, token_decimals: u8) -> Result SCALE` +/// - `DomainError` if a balance/input weight is zero, `fee_rate > SCALE`, +/// the post-trade input balance ratio is below 1%, or the weight ratio exceeds 20 /// - `Overflow` if `balance_in + amount_in` overflows or power computation fails /// +/// Every rounding step is directed against the trader. The power output is +/// raised by its certified five-unit absolute error bound before payout. +/// /// # Example /// ``` /// use solmath::{weighted_pool_swap, SCALE}; @@ -153,12 +166,28 @@ pub fn weighted_pool_swap( } // ratio = B_i / (B_i + a_in) - let denominator = balance_in.checked_add(amount_in).ok_or(SolMathError::Overflow)?; - let ratio = fp_div(balance_in, denominator)?; + let denominator = balance_in + .checked_add(amount_in) + .ok_or(SolMathError::Overflow)?; + let min_balance = denominator / 100 + u128::from(denominator % 100 != 0); + if balance_in < min_balance { + return Err(SolMathError::DomainError); + } + let weight_q = weight_in / weight_out; + if weight_q > 20 || (weight_q == 20 && weight_in % weight_out != 0) { + return Err(SolMathError::DomainError); + } + let ratio = fp_div_ceil(balance_in, denominator)?; // weight_ratio = w_i / w_j let weight_ratio = fp_div(weight_in, weight_out)?; + // The certified absolute error bound used below applies to this domain. + // Reject unsupported pool shapes rather than using an empirical margin. + if ratio < SCALE / 100 || weight_ratio > 20 * SCALE { + return Err(SolMathError::DomainError); + } + // power = ratio ^ weight_ratio (using HP for precision) let power = pow_fixed_hp(ratio, weight_ratio)?; @@ -168,9 +197,13 @@ pub fn weighted_pool_swap( if power > SCALE { return Err(SolMathError::Overflow); } - let one_minus_power = SCALE - power; + // Within ratio∈[0.01,1], exponent∈[0,20], and output≤1, Proposition 11's + // component-wise bound is 5 raw SCALE units. Move the result upward by + // that full bound so approximation error cannot favour the trader. + let power_up = power.checked_add(5).unwrap_or(SCALE).min(SCALE); + let one_minus_power = SCALE - power_up; // gross_out rounds DOWN (trader gets less) — protocol-favorable - let gross_out = fp_mul(balance_out, one_minus_power)?; + let gross_out = fp_mul(balance_out, one_minus_power)?.min(balance_out.saturating_sub(1)); // fee rounds UP (protocol collects at least the fee) — protocol-favorable let fee = mul_div_ceil_u128(gross_out, fee_rate, SCALE)?; @@ -184,3 +217,47 @@ pub fn weighted_pool_swap( Ok((net_out, fee)) } + +#[cfg(test)] +mod adversarial_tests { + use super::*; + + #[test] + fn swap_rounding_never_exceeds_exact_equal_weight_output() { + let (out, fee) = + weighted_pool_swap(SCALE, 3 * SCALE, SCALE / 2, SCALE / 2, 2 * SCALE, 0).unwrap(); + assert_eq!(fee, 0); + assert!(out <= 2 * SCALE); + } + + #[test] + fn rejects_outside_certified_power_domain() { + assert_eq!( + weighted_pool_swap(SCALE, SCALE, 21 * SCALE, SCALE, SCALE, 0), + Err(SolMathError::DomainError) + ); + assert_eq!( + weighted_pool_swap(SCALE, SCALE, SCALE, SCALE, 100 * SCALE, 0), + Err(SolMathError::DomainError) + ); + } + + #[test] + fn exact_domain_checks_cannot_be_bypassed_by_fixed_rounding() { + assert_eq!( + weighted_pool_swap(100_000_000, SCALE, SCALE, SCALE, 9_900_000_001, 0,), + Err(SolMathError::DomainError) + ); + assert_eq!( + weighted_pool_swap( + SCALE, + SCALE, + 20_000_000_000_000_000_001, + 1_000_000_000_000_000_000, + SCALE, + 0, + ), + Err(SolMathError::DomainError) + ); + } +} diff --git a/src/rainbow.rs b/src/rainbow.rs new file mode 100644 index 0000000..f2b7b23 --- /dev/null +++ b/src/rainbow.rs @@ -0,0 +1,194 @@ +//! Two-asset (rainbow) option pricing via the bivariate normal CDF. +//! +//! Worst-of / best-of options on two assets have analytic Stulz (1982) formulas +//! built from the bivariate normal CDF. SolMath evaluates those formulas with +//! its deterministic `bvn_cdf` kernel, so pricing needs no lattice, Monte Carlo +//! simulation, or off-chain surface. +//! +//! [`worst_of_call`] prices a call on `min(S1, S2)`; [`best_of_call`] a call on +//! `max(S1, S2)`. Each asset carries its own continuous dividend yield; `rho` is +//! the return correlation and may be negative. All values are fixed-point at +//! `SCALE = 1e12`; `rho` is signed at SCALE. +//! +//! Validated against Monte Carlo (6M paths) and the Stulz analytic reference to +//! MC noise (~1e-3) across positive and negative correlation. + +use crate::arithmetic::{fp_div_i, fp_mul_i, fp_sqrt}; +use crate::bvn_cdf::bvn_cdf; +use crate::constants::{SCALE, SCALE_I}; +use crate::error::SolMathError; +use crate::transcendental::{exp_fixed_i, ln_fixed_i}; + +const MAX_INPUT: u128 = 100_000 * SCALE; + +/// Shared, validated intermediates for both min and max payoffs. +struct RainbowTerms { + s1: i128, + s2: i128, + disc_q1: i128, // e^{-q1 T} + disc_q2: i128, // e^{-q2 T} + disc_r_k: i128, // K e^{-r T} + y1: i128, + y2: i128, + d: i128, + srt: i128, // σ√T with σ = √(σ1²+σ2²-2ρσ1σ2) + v1_sqrt_t: i128, + v2_sqrt_t: i128, + r1: i128, // corr for the S1 term + r2: i128, // corr for the S2 term + rho: i128, +} + +#[allow(clippy::too_many_arguments)] +fn prepare( + s1: u128, + s2: u128, + k: u128, + r: u128, + q1: u128, + q2: u128, + sigma1: u128, + sigma2: u128, + rho: i128, + t: u128, +) -> Result { + if s1 > MAX_INPUT + || s2 > MAX_INPUT + || k > MAX_INPUT + || r > MAX_INPUT + || q1 > MAX_INPUT + || q2 > MAX_INPUT + || sigma1 > MAX_INPUT + || sigma2 > MAX_INPUT + || t > MAX_INPUT + { + return Err(SolMathError::Overflow); + } + if s1 == 0 || s2 == 0 || k == 0 || sigma1 == 0 || sigma2 == 0 || t == 0 { + return Err(SolMathError::DomainError); + } + if rho.unsigned_abs() >= SCALE { + return Err(SolMathError::DomainError); + } + let (s1, s2, k, r) = (s1 as i128, s2 as i128, k as i128, r as i128); + let (q1, q2, sigma1, sigma2, t) = ( + q1 as i128, + q2 as i128, + sigma1 as i128, + sigma2 as i128, + t as i128, + ); + let b1 = r - q1; + let b2 = r - q2; + + let sqrt_t = fp_sqrt(t as u128)? as i128; + let s1_sq = fp_mul_i(sigma1, sigma1)?; + let s2_sq = fp_mul_i(sigma2, sigma2)?; + // σ² = σ1² + σ2² - 2ρσ1σ2 + let cross = fp_mul_i(fp_mul_i(rho, sigma1)?, sigma2)?; + let sig_sq = s1_sq + s2_sq - 2 * cross; + if sig_sq <= 0 { + return Err(SolMathError::DomainError); + } + let sig = fp_sqrt(sig_sq as u128)? as i128; + let srt = fp_mul_i(sig, sqrt_t)?; + let v1_sqrt_t = fp_mul_i(sigma1, sqrt_t)?; + let v2_sqrt_t = fp_mul_i(sigma2, sqrt_t)?; + if srt == 0 || v1_sqrt_t == 0 || v2_sqrt_t == 0 { + return Err(SolMathError::DomainError); + } + + // d = [ln(S1/S2) + (b1 - b2 + σ²/2)T] / (σ√T) + let ln_s1s2 = ln_fixed_i(fp_div_i(s1, s2)? as u128)?; + let d = fp_div_i(ln_s1s2 + fp_mul_i(b1 - b2 + sig_sq / 2, t)?, srt)?; + // y_i = [ln(S_i/K) + (b_i + σ_i²/2)T] / (σ_i√T) + let y1 = fp_div_i( + ln_fixed_i(fp_div_i(s1, k)? as u128)? + fp_mul_i(b1 + s1_sq / 2, t)?, + v1_sqrt_t, + )?; + let y2 = fp_div_i( + ln_fixed_i(fp_div_i(s2, k)? as u128)? + fp_mul_i(b2 + s2_sq / 2, t)?, + v2_sqrt_t, + )?; + // r1 = (ρσ2 - σ1)/σ, r2 = (ρσ1 - σ2)/σ + let r1 = fp_div_i(fp_mul_i(rho, sigma2)? - sigma1, sig)?; + let r2 = fp_div_i(fp_mul_i(rho, sigma1)? - sigma2, sig)?; + + Ok(RainbowTerms { + s1, + s2, + disc_q1: exp_fixed_i(-fp_mul_i(q1, t)?)?, + disc_q2: exp_fixed_i(-fp_mul_i(q2, t)?)?, + disc_r_k: fp_mul_i(k, exp_fixed_i(-fp_mul_i(r, t)?)?)?, + y1, + y2, + d, + srt, + v1_sqrt_t, + v2_sqrt_t, + r1, + r2, + rho, + }) +} + +/// Call on the **minimum** of two assets: `e^{-rT} E[max(min(S1,S2) - K, 0)]`. +/// +/// Continuous dividend yields `q1`, `q2`; `rho` the (signed) return correlation. +/// Exact Stulz closed form via three `bvn_cdf` evaluations. +#[allow(clippy::too_many_arguments)] +pub fn worst_of_call( + s1: u128, + s2: u128, + k: u128, + r: u128, + q1: u128, + q2: u128, + sigma1: u128, + sigma2: u128, + rho: i128, + t: u128, +) -> Result { + let tm = prepare(s1, s2, k, r, q1, q2, sigma1, sigma2, rho, t)?; + // Cmin = S1 e^{-q1T} M(y1,-d;r1) + S2 e^{-q2T} M(y2,d-σ√T;r2) + // - K e^{-rT} M(y1-σ1√T, y2-σ2√T; ρ) + let a = fp_mul_i(fp_mul_i(tm.s1, tm.disc_q1)?, bvn_cdf(tm.y1, -tm.d, tm.r1)?)?; + let b = fp_mul_i( + fp_mul_i(tm.s2, tm.disc_q2)?, + bvn_cdf(tm.y2, tm.d - tm.srt, tm.r2)?, + )?; + let c = fp_mul_i( + tm.disc_r_k, + bvn_cdf(tm.y1 - tm.v1_sqrt_t, tm.y2 - tm.v2_sqrt_t, tm.rho)?, + )?; + Ok((a + b - c).max(0) as u128) +} + +/// Call on the **maximum** of two assets: `e^{-rT} E[max(max(S1,S2) - K, 0)]`. +/// +/// Exact Stulz closed form via three `bvn_cdf` evaluations. +#[allow(clippy::too_many_arguments)] +pub fn best_of_call( + s1: u128, + s2: u128, + k: u128, + r: u128, + q1: u128, + q2: u128, + sigma1: u128, + sigma2: u128, + rho: i128, + t: u128, +) -> Result { + let tm = prepare(s1, s2, k, r, q1, q2, sigma1, sigma2, rho, t)?; + // Cmax = S1 e^{-q1T} M(y1,d;-r1) + S2 e^{-q2T} M(y2,-d+σ√T;-r2) + // - K e^{-rT} [1 - M(-y1+σ1√T, -y2+σ2√T; ρ)] + let a = fp_mul_i(fp_mul_i(tm.s1, tm.disc_q1)?, bvn_cdf(tm.y1, tm.d, -tm.r1)?)?; + let b = fp_mul_i( + fp_mul_i(tm.s2, tm.disc_q2)?, + bvn_cdf(tm.y2, tm.srt - tm.d, -tm.r2)?, + )?; + let m = bvn_cdf(-tm.y1 + tm.v1_sqrt_t, -tm.y2 + tm.v2_sqrt_t, tm.rho)?; + let c = fp_mul_i(tm.disc_r_k, SCALE_I - m)?; + Ok((a + b - c).max(0) as u128) +} diff --git a/src/sabr.rs b/src/sabr.rs index 2ee4323..b7a3cff 100644 --- a/src/sabr.rs +++ b/src/sabr.rs @@ -1,8 +1,504 @@ +use crate::arithmetic::{ + cmp_wide, fp_div, fp_div_i, fp_mul, fp_mul_i, fp_mul_i_fast, fp_sqrt, wide_mul_u128, +}; use crate::constants::*; use crate::error::SolMathError; -use crate::arithmetic::{fp_mul, fp_mul_i, fp_mul_i_fast, fp_div, fp_div_i, fp_sqrt}; -use crate::transcendental::{ln_fixed_i, exp_fixed_i}; -use crate::hp::{pow_fixed_hp, bs_full_hp}; +use crate::hp::{bs_full_hp, pow_fixed_hp}; +use crate::transcendental::{exp_fixed_i, ln_fixed_i}; + +// ============================================================ +// Whole-surface certification API +// ============================================================ + +/// Maximum number of strikes accepted by [`certify_sabr_surface`]. +pub const MAX_SABR_SURFACE_STRIKES: usize = 32; + +/// Maximum number of maturities accepted by [`certify_sabr_surface`]. +pub const MAX_SABR_SURFACE_MATURITIES: usize = 16; + +/// Maximum number of quote pairs accepted by [`certify_sabr_surface`]. +/// +/// These limits bound all validation loops and discount-factor evaluations. +/// Raise them only after re-metering the target SBF program. +pub const MAX_SABR_SURFACE_QUOTES: usize = 256; + +/// One immutable quote read from a [`CertifiedSabrSurface`]. +/// +/// Values are at [`SCALE`]. The quote is returned from the caller-supplied +/// surface that was certified atomically; no SABR repricing occurs here. Its +/// fields are private so safe callers cannot forge a certified execution +/// value. Execution entrypoints should accept this type, or a certificate and +/// grid indices, instead of accepting raw quote fields independently. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CertifiedSabrQuote { + spot: u128, + rate: u128, + strike: u128, + maturity: u128, + call: u128, + put: u128, +} + +impl CertifiedSabrQuote { + /// Spot against which this quote was certified. + pub fn spot(&self) -> u128 { + self.spot + } + + /// Rate against which this quote was certified. + pub fn rate(&self) -> u128 { + self.rate + } + + /// Strike of this certified grid node. + pub fn strike(&self) -> u128 { + self.strike + } + + /// Maturity of this certified grid node. + pub fn maturity(&self) -> u128 { + self.maturity + } + + /// Stored certified call value. + pub fn call(&self) -> u128 { + self.call + } + + /// Stored certified put value. + pub fn put(&self) -> u128 { + self.put + } +} + +/// Borrowed proof that a complete rectangular option grid passed static +/// no-arbitrage validation. +/// +/// The underlying slices remain immutably borrowed for the certificate's +/// lifetime, so safe Rust cannot mutate certified values before execution +/// consumes them. Quotes are stored maturity-major: index +/// `maturity_index * strike_count + strike_index`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CertifiedSabrSurface<'a> { + spot: u128, + rate: u128, + strikes: &'a [u128], + maturities: &'a [u128], + calls: &'a [u128], + puts: &'a [u128], +} + +impl<'a> CertifiedSabrSurface<'a> { + /// Spot shared by the certified grid. + pub fn spot(&self) -> u128 { + self.spot + } + + /// Non-negative continuously-compounded rate shared by the grid. + pub fn rate(&self) -> u128 { + self.rate + } + + /// Strictly increasing certified strike axis. + pub fn strikes(&self) -> &'a [u128] { + self.strikes + } + + /// Strictly increasing certified maturity axis. + pub fn maturities(&self) -> &'a [u128] { + self.maturities + } + + /// Number of immutable quotes in the certificate. + pub fn quote_count(&self) -> usize { + self.calls.len() + } + + /// Read a certified quote by `(maturity_index, strike_index)`. + /// + /// This returns the stored, certified value and never calls + /// [`sabr_price`]. Invalid indices return `DomainError`. + pub fn quote_at( + &self, + maturity_index: usize, + strike_index: usize, + ) -> Result { + if maturity_index >= self.maturities.len() || strike_index >= self.strikes.len() { + return Err(SolMathError::DomainError); + } + let index = maturity_index + .checked_mul(self.strikes.len()) + .and_then(|v| v.checked_add(strike_index)) + .ok_or(SolMathError::Overflow)?; + Ok(CertifiedSabrQuote { + spot: self.spot, + rate: self.rate, + strike: self.strikes[strike_index], + maturity: self.maturities[maturity_index], + call: self.calls[index], + put: self.puts[index], + }) + } +} + +/// Atomically certify a complete ordered SABR quote surface. +/// +/// `calls` and `puts` must be rectangular, maturity-major grids with exactly +/// `maturities.len() * strikes.len()` entries. The function requires at least +/// three strikes (so butterflies can be checked) and two maturities (so +/// calendars can be checked). It validates every entry before constructing the +/// returned immutable certificate. Axes and quote count must not exceed +/// [`MAX_SABR_SURFACE_STRIKES`], [`MAX_SABR_SURFACE_MATURITIES`], and +/// [`MAX_SABR_SURFACE_QUOTES`]; resource-limit violations return `DomainError`. +/// +/// The certificate enforces: +/// - strictly increasing, non-zero strikes and strictly increasing maturities; +/// - exact fixed-point put-call parity against `K * exp(-rT)`; +/// - hard call and put bounds; +/// - calls non-increasing and puts non-decreasing in strike; +/// - non-negative call and put butterflies on discounted, irregular strike +/// spacing, including the synthetic zero-strike values `(call=spot, put=0)`; +/// and +/// - piecewise-linear calls non-decreasing in maturity at equal discounted +/// strike over adjacent rows' common quoted support (non-negative rates, no +/// dividends). +/// +/// This certifies the supplied values, not their model provenance. Generate or +/// calibrate SABR quotes off-chain, project rounding noise to the exact static +/// constraints, certify once, then execute only values read via +/// [`CertifiedSabrSurface::quote_at`]. +pub fn certify_sabr_surface<'a>( + spot: u128, + rate: u128, + strikes: &'a [u128], + maturities: &'a [u128], + calls: &'a [u128], + puts: &'a [u128], +) -> Result, SolMathError> { + if spot > i128::MAX as u128 || rate > i128::MAX as u128 { + return Err(SolMathError::Overflow); + } + if spot == 0 { + return Err(SolMathError::DomainError); + } + if strikes.len() < 3 + || maturities.len() < 2 + || strikes.len() > MAX_SABR_SURFACE_STRIKES + || maturities.len() > MAX_SABR_SURFACE_MATURITIES + { + return Err(SolMathError::DomainError); + } + let quote_count = strikes + .len() + .checked_mul(maturities.len()) + .ok_or(SolMathError::Overflow)?; + if quote_count > MAX_SABR_SURFACE_QUOTES + || calls.len() != quote_count + || puts.len() != quote_count + { + return Err(SolMathError::DomainError); + } + + let mut strike_index = 0usize; + while strike_index < strikes.len() { + let strike = strikes[strike_index]; + if strike > i128::MAX as u128 { + return Err(SolMathError::Overflow); + } + if strike == 0 { + return Err(SolMathError::DomainError); + } + if strike_index > 0 && strike <= strikes[strike_index - 1] { + return Err(SolMathError::DomainError); + } + strike_index += 1; + } + + let mut discounts = [0i128; MAX_SABR_SURFACE_MATURITIES]; + let mut maturity_index = 0usize; + while maturity_index < maturities.len() { + let maturity = maturities[maturity_index]; + if maturity > i128::MAX as u128 { + return Err(SolMathError::Overflow); + } + if maturity_index > 0 && maturity <= maturities[maturity_index - 1] { + return Err(SolMathError::DomainError); + } + + let discount = sabr_surface_discount(rate, maturity)?; + discounts[maturity_index] = discount; + let mut discounted_strikes = [0u128; MAX_SABR_SURFACE_STRIKES]; + fill_discounted_strikes(strikes, discount, &mut discounted_strikes)?; + let row_start = maturity_index + .checked_mul(strikes.len()) + .ok_or(SolMathError::Overflow)?; + let mut column = 0usize; + while column < strikes.len() { + let index = row_start + .checked_add(column) + .ok_or(SolMathError::Overflow)?; + validate_sabr_surface_quote( + spot, + discounted_strikes[column], + calls[index], + puts[index], + )?; + column += 1; + } + let row_end = row_start + .checked_add(strikes.len()) + .ok_or(SolMathError::Overflow)?; + validate_sabr_surface_row( + spot, + &discounted_strikes[..strikes.len()], + &calls[row_start..row_end], + &puts[row_start..row_end], + )?; + maturity_index += 1; + } + + // With a non-negative rate and no dividends, the discounted stock is a + // martingale. Calendar comparisons therefore use equal discounted strike, + // not equal nominal strike. Checking both rows' breakpoints is sufficient: + // their piecewise-linear difference is linear between the union points. + maturity_index = 1; + while maturity_index < maturities.len() { + let previous = (maturity_index - 1) + .checked_mul(strikes.len()) + .ok_or(SolMathError::Overflow)?; + let current = maturity_index + .checked_mul(strikes.len()) + .ok_or(SolMathError::Overflow)?; + validate_sabr_surface_calendar( + spot, + strikes, + discounts[maturity_index - 1], + &calls[previous..previous + strikes.len()], + discounts[maturity_index], + &calls[current..current + strikes.len()], + )?; + maturity_index += 1; + } + + Ok(CertifiedSabrSurface { + spot, + rate, + strikes, + maturities, + calls, + puts, + }) +} + +fn sabr_surface_discount(rate: u128, maturity: u128) -> Result { + let rt = fp_mul_i(rate as i128, maturity as i128)?; + exp_fixed_i(rt.checked_neg().ok_or(SolMathError::Overflow)?) +} + +fn fill_discounted_strikes( + strikes: &[u128], + discount: i128, + output: &mut [u128; MAX_SABR_SURFACE_STRIKES], +) -> Result<(), SolMathError> { + if discount <= 0 { + return Err(SolMathError::NoConvergence); + } + let mut index = 0usize; + while index < strikes.len() { + let discounted = fp_mul_i(strikes[index] as i128, discount)?; + if discounted <= 0 { + return Err(SolMathError::NoConvergence); + } + let discounted = discounted as u128; + if index > 0 && discounted <= output[index - 1] { + return Err(SolMathError::NoConvergence); + } + output[index] = discounted; + index += 1; + } + Ok(()) +} + +fn validate_sabr_surface_quote( + spot: u128, + discounted_strike: u128, + call: u128, + put: u128, +) -> Result<(), SolMathError> { + let call_lower = spot.saturating_sub(discounted_strike); + let put_lower = discounted_strike.saturating_sub(spot); + if call < call_lower || call > spot || put < put_lower || put > discounted_strike { + return Err(SolMathError::NoConvergence); + } + let parity_left = call + .checked_add(discounted_strike) + .ok_or(SolMathError::Overflow)?; + let parity_right = put.checked_add(spot).ok_or(SolMathError::Overflow)?; + if parity_left != parity_right { + return Err(SolMathError::NoConvergence); + } + Ok(()) +} + +fn validate_sabr_surface_row( + spot: u128, + discounted_strikes: &[u128], + calls: &[u128], + puts: &[u128], +) -> Result<(), SolMathError> { + let mut i = 1usize; + while i < discounted_strikes.len() { + if calls[i] > calls[i - 1] || puts[i] < puts[i - 1] { + return Err(SolMathError::NoConvergence); + } + i += 1; + } + + let mut strike_left = discounted_strikes[0]; + let mut call_drop_left = spot + .checked_sub(calls[0]) + .ok_or(SolMathError::NoConvergence)?; + let mut put_rise_left = puts[0]; + i = 0; + while i + 1 < discounted_strikes.len() { + let strike_right = discounted_strikes[i + 1] + .checked_sub(discounted_strikes[i]) + .ok_or(SolMathError::Overflow)?; + let call_drop_right = calls[i] + .checked_sub(calls[i + 1]) + .ok_or(SolMathError::NoConvergence)?; + let call_left = wide_mul_u128(call_drop_left, strike_right); + let call_right = wide_mul_u128(call_drop_right, strike_left); + if cmp_wide(call_left, call_right).is_lt() { + return Err(SolMathError::NoConvergence); + } + + let put_rise_right = puts[i + 1] + .checked_sub(puts[i]) + .ok_or(SolMathError::NoConvergence)?; + let put_left = wide_mul_u128(put_rise_left, strike_right); + let put_right = wide_mul_u128(put_rise_right, strike_left); + if cmp_wide(put_left, put_right).is_gt() { + return Err(SolMathError::NoConvergence); + } + strike_left = strike_right; + call_drop_left = call_drop_right; + put_rise_left = put_rise_right; + i += 1; + } + Ok(()) +} + +fn add_wide(left: (u128, u128), right: (u128, u128)) -> Result<(u128, u128), SolMathError> { + let (low, carry) = left.1.overflowing_add(right.1); + let high = left + .0 + .checked_add(right.0) + .and_then(|value| value.checked_add(carry as u128)) + .ok_or(SolMathError::Overflow)?; + Ok((high, low)) +} + +fn compare_piecewise_call_to_value( + spot: u128, + discounted_strikes: &[u128], + calls: &[u128], + discounted_strike: u128, + value: u128, +) -> Result { + if discounted_strikes.len() != calls.len() + || discounted_strikes.is_empty() + || discounted_strike > discounted_strikes[discounted_strikes.len() - 1] + { + return Err(SolMathError::DomainError); + } + + let mut right_index = 0usize; + while discounted_strikes[right_index] < discounted_strike { + right_index += 1; + } + if discounted_strikes[right_index] == discounted_strike { + return Ok(calls[right_index].cmp(&value)); + } + + let (left_strike, left_call) = if right_index == 0 { + (0, spot) + } else { + (discounted_strikes[right_index - 1], calls[right_index - 1]) + }; + let right_strike = discounted_strikes[right_index]; + let right_call = calls[right_index]; + let width = right_strike + .checked_sub(left_strike) + .ok_or(SolMathError::Overflow)?; + let left_weight = right_strike + .checked_sub(discounted_strike) + .ok_or(SolMathError::Overflow)?; + let right_weight = discounted_strike + .checked_sub(left_strike) + .ok_or(SolMathError::Overflow)?; + let numerator = add_wide( + wide_mul_u128(left_call, left_weight), + wide_mul_u128(right_call, right_weight), + )?; + Ok(cmp_wide(numerator, wide_mul_u128(value, width))) +} + +fn validate_sabr_surface_calendar( + spot: u128, + strikes: &[u128], + previous_discount: i128, + previous_calls: &[u128], + current_discount: i128, + current_calls: &[u128], +) -> Result<(), SolMathError> { + let mut previous_strikes = [0u128; MAX_SABR_SURFACE_STRIKES]; + let mut current_strikes = [0u128; MAX_SABR_SURFACE_STRIKES]; + fill_discounted_strikes(strikes, previous_discount, &mut previous_strikes)?; + fill_discounted_strikes(strikes, current_discount, &mut current_strikes)?; + let previous_strikes = &previous_strikes[..strikes.len()]; + let current_strikes = ¤t_strikes[..strikes.len()]; + let common_max = previous_strikes[previous_strikes.len() - 1] + .min(current_strikes[current_strikes.len() - 1]); + + // At every previous-row breakpoint in the common support, the later curve + // must be at least the earlier stored call. + let mut index = 0usize; + while index < previous_strikes.len() && previous_strikes[index] <= common_max { + if compare_piecewise_call_to_value( + spot, + current_strikes, + current_calls, + previous_strikes[index], + previous_calls[index], + )? + .is_lt() + { + return Err(SolMathError::NoConvergence); + } + index += 1; + } + + // At every later-row breakpoint in the common support, the earlier curve + // must be no greater than the later stored call. Together with the loop + // above this checks the union of all piecewise-linear breakpoints. + index = 0; + while index < current_strikes.len() && current_strikes[index] <= common_max { + if compare_piecewise_call_to_value( + spot, + previous_strikes, + previous_calls, + current_strikes[index], + current_calls[index], + )? + .is_gt() + { + return Err(SolMathError::NoConvergence); + } + index += 1; + } + Ok(()) +} // ============================================================ // Single-strike API @@ -25,8 +521,9 @@ use crate::hp::{pow_fixed_hp, bs_full_hp}; /// # Returns /// Implied Black vol at SCALE. Returns 0 for zero inputs. /// -/// # Accuracy -/// 0.5% tolerance vs QuantLib. 36-43K CU per strike. +/// # Accuracy and CU +/// The final 100,000-case corpus observed max/P99/median errors 660/102/3 raw +/// volatility units. Final SBF audit: 51,516 CU average, 74,338 max. /// /// # Example /// ``` @@ -40,11 +537,20 @@ use crate::hp::{pow_fixed_hp, bs_full_hp}; /// # Ok::<(), solmath::SolMathError>(()) /// ``` pub fn sabr_implied_vol( - f: u128, k: u128, t: u128, - alpha: u128, beta: u128, rho: i128, nu: u128, + f: u128, + k: u128, + t: u128, + alpha: u128, + beta: u128, + rho: i128, + nu: u128, ) -> Result { - if f > i128::MAX as u128 || k > i128::MAX as u128 || t > i128::MAX as u128 - || alpha > i128::MAX as u128 || beta > i128::MAX as u128 || nu > i128::MAX as u128 + if f > i128::MAX as u128 + || k > i128::MAX as u128 + || t > i128::MAX as u128 + || alpha > i128::MAX as u128 + || beta > i128::MAX as u128 + || nu > i128::MAX as u128 { return Err(SolMathError::Overflow); } @@ -57,7 +563,6 @@ pub fn sabr_implied_vol( if rho <= -SCALE_I || rho >= SCALE_I { return Err(SolMathError::DomainError); } - let s = SCALE; let si = SCALE_I; @@ -67,11 +572,11 @@ pub fn sabr_implied_vol( let alpha_i = alpha as i128; let nu_i = nu as i128; - // ATM detection: |F−K| < 0.0001·F - let atm_threshold = f / 10_000; - let is_atm = if f > k { f - k < atm_threshold } else { k - f < atm_threshold }; + // Use the ATM limit only at exact ATM. A tolerance branch introduced a + // discontinuity in executable quotes at its boundary. + let is_atm = f == k; - if is_atm || nu == 0 { + if is_atm { let f_pow = if one_minus_beta == 0 { s } else if one_minus_beta == s { @@ -82,16 +587,21 @@ pub fn sabr_implied_vol( let base_vol = fp_div_i(alpha_i, f_pow as i128)?; - if nu == 0 { - return Ok(if base_vol > 0 { base_vol as u128 } else { 0 }); - } - let h = compute_h(one_minus_beta, alpha, beta_i, rho, nu_i, alpha_i, f_pow)?; // h ∈ [-SCALE_I, SCALE_I] by design; fp_mul_i(h, t) ≤ SCALE_I for t ≤ SCALE_I; // si + correction ≤ 2·SCALE_I. Fits i128. - let correction = si + fp_mul_i(h, t as i128)?; + let correction = si + .checked_add(fp_mul_i(h, t as i128)?) + .ok_or(SolMathError::Overflow)?; + if correction <= 0 { + return Err(SolMathError::NoConvergence); + } let sigma_i = fp_mul_i(base_vol, correction)?; - return Ok(if sigma_i > 0 { sigma_i as u128 } else { 0 }); + return if sigma_i > 0 { + Ok(sigma_i as u128) + } else { + Err(SolMathError::NoConvergence) + }; } // --- General (OTM/ITM) formula --- @@ -110,7 +620,17 @@ pub fn sabr_implied_vol( let fk_ratio = fp_div(f, k)?; let log_fk = ln_fixed_i(fk_ratio)?; - sabr_assemble(f_mid_pow, log_fk, one_minus_beta, alpha, alpha_i, beta_i, rho, nu_i, t) + sabr_assemble( + f_mid_pow, + log_fk, + one_minus_beta, + alpha, + alpha_i, + beta_i, + rho, + nu_i, + t, + ) } /// SABR European option price via implied vol into Black-Scholes. @@ -128,18 +648,33 @@ pub fn sabr_implied_vol( /// # Returns /// `(call, put)` prices at SCALE. /// -/// # CU Cost -/// 158-171K CU. -pub fn sabr_price( - s: u128, k: u128, r: u128, t: u128, - alpha: u128, beta: u128, rho: i128, nu: u128, +/// Internal component of the guarded public price; not a standalone CU +/// contract. Meter [`sabr_price`] in the consuming program. +fn sabr_price_raw( + s: u128, + k: u128, + r: u128, + t: u128, + alpha: u128, + beta: u128, + rho: i128, + nu: u128, ) -> Result<(u128, u128), SolMathError> { - if s > i128::MAX as u128 || k > i128::MAX as u128 || r > i128::MAX as u128 - || t > i128::MAX as u128 || alpha > i128::MAX as u128 - || beta > i128::MAX as u128 || nu > i128::MAX as u128 + if s > i128::MAX as u128 + || k > i128::MAX as u128 + || r > i128::MAX as u128 + || t > i128::MAX as u128 + || alpha > i128::MAX as u128 + || beta > i128::MAX as u128 + || nu > i128::MAX as u128 { return Err(SolMathError::Overflow); } + if fp_mul(fp_mul(nu, nu)?, t)? > SCALE / 2 { + // Executable prices fail closed outside the supported first-order + // asymptotic regime. Analytics-only implied vol remains available. + return Err(SolMathError::NoConvergence); + } let r_t = fp_mul_i(r as i128, t as i128)?; let f = fp_mul_i(s as i128, exp_fixed_i(r_t)?)? as u128; let sigma = sabr_implied_vol(f, k, t, alpha, beta, rho, nu)?; @@ -153,6 +688,76 @@ pub fn sabr_price( Ok((bs.call, bs.put)) } +/// SABR European price with a local static-arbitrage guard. +/// +/// In addition to hard Black-Scholes bounds, prices are sampled at adjacent +/// strikes and rejected if calls rise with strike or violate local convexity. +/// A calibrated quote grid still needs a full surface-level arbitrage check. +/// Value-bearing execution must assemble the complete grid, call +/// [`certify_sabr_surface`], and consume only [`CertifiedSabrQuote`] values; +/// this isolated return is analytics/input to that workflow, not a global +/// no-arbitrage certificate. +/// Executable pricing also requires `nu²T <= 0.5`; implied-vol analytics are +/// available outside that range but are not certified for execution. +/// This safety check evaluates three prices; budget approximately 3× the raw +/// single-strike SABR+BS cost. +/// Final accepted-case SBF audit: 390,137 CU average, 639,037 P99, +/// 643,745 max. +pub fn sabr_price( + s: u128, + k: u128, + r: u128, + t: u128, + alpha: u128, + beta: u128, + rho: i128, + nu: u128, +) -> Result<(u128, u128), SolMathError> { + let price = sabr_price_raw(s, k, r, t, alpha, beta, rho, nu)?; + validate_sabr_local(s, k, r, t, alpha, beta, rho, nu, price)?; + Ok(price) +} + +#[allow(clippy::too_many_arguments)] +fn validate_sabr_local( + s: u128, + k: u128, + r: u128, + t: u128, + alpha: u128, + beta: u128, + rho: i128, + nu: u128, + price: (u128, u128), +) -> Result<(), SolMathError> { + if k == 0 || t == 0 || s == 0 { + return Ok(()); + } + // Keep the three strikes exactly equally spaced, including for tiny K. + // A saturating lower point with a larger fixed step invalidates the + // discrete convexity inequality and falsely rejects valid prices. + let step = (k / 1_000).max(1).min(k); + let k_lo = k - step; + let k_hi = k.checked_add(step).ok_or(SolMathError::Overflow)?; + let lo = sabr_price_raw(s, k_lo, r, t, alpha, beta, rho, nu)?; + let hi = sabr_price_raw(s, k_hi, r, t, alpha, beta, rho, nu)?; + const ROUNDING_TOLERANCE: u128 = 1_000; + if lo.0.saturating_add(ROUNDING_TOLERANCE) < price.0 + || price.0.saturating_add(ROUNDING_TOLERANCE) < hi.0 + { + return Err(SolMathError::NoConvergence); + } + let chord = + lo.0.checked_add(hi.0) + .and_then(|v| v.checked_add(ROUNDING_TOLERANCE)) + .ok_or(SolMathError::Overflow)?; + let twice = price.0.checked_mul(2).ok_or(SolMathError::Overflow)?; + if chord < twice { + return Err(SolMathError::NoConvergence); + } + Ok(()) +} + /// Full SABR Greeks via BS(sigma_SABR). Sticky-strike sensitivities. /// /// Computes SABR implied vol then returns the full [`BsFull`] struct @@ -164,15 +769,30 @@ pub fn sabr_price( /// # Returns /// [`BsFull`] with prices and all first-order Greeks at SCALE. /// +/// The embedded price has only the isolated local guard. Value-bearing +/// execution must separately certify the complete price grid with +/// [`certify_sabr_surface`] before acting on this output. +/// /// # CU Cost -/// ~160-175K CU. +/// Final accepted-case SBF audit: 390,165 CU average, 639,065 P99, +/// 643,773 max, including neighbouring safety quotes. pub fn sabr_greeks( - s: u128, k: u128, r: u128, t: u128, - alpha: u128, beta: u128, rho: i128, nu: u128, + s: u128, + k: u128, + r: u128, + t: u128, + alpha: u128, + beta: u128, + rho: i128, + nu: u128, ) -> Result { - if s > i128::MAX as u128 || k > i128::MAX as u128 || r > i128::MAX as u128 - || t > i128::MAX as u128 || alpha > i128::MAX as u128 - || beta > i128::MAX as u128 || nu > i128::MAX as u128 + if s > i128::MAX as u128 + || k > i128::MAX as u128 + || r > i128::MAX as u128 + || t > i128::MAX as u128 + || alpha > i128::MAX as u128 + || beta > i128::MAX as u128 + || nu > i128::MAX as u128 { return Err(SolMathError::Overflow); } @@ -180,16 +800,49 @@ pub fn sabr_greeks( if t == 0 { return Err(SolMathError::DomainError); } + if fp_mul(fp_mul(nu, nu)?, t)? > SCALE / 2 { + return Err(SolMathError::NoConvergence); + } let r_t = fp_mul_i(r as i128, t as i128)?; let f = fp_mul_i(s as i128, exp_fixed_i(r_t)?)? as u128; let sigma = sabr_implied_vol(f, k, t, alpha, beta, rho, nu)?; if sigma == 0 { let k_disc = fp_mul_i(k as i128, exp_fixed_i(-r_t)?)? as u128; - let call = if s > k_disc { s - k_disc } else { 0 }; - let put = if k_disc > s { k_disc - s } else { 0 }; - return Ok(BsFull { call, put, call_delta: 0, put_delta: 0, gamma: 0, vega: 0, call_theta: 0, put_theta: 0, call_rho: 0, put_rho: 0 }); + if s == k_disc { + return Err(SolMathError::DomainError); // kink: delta is undefined + } + let r_kd = fp_mul_i(r as i128, k_disc as i128)?; + let t_kd = fp_mul_i(t as i128, k_disc as i128)?; + if s > k_disc { + return Ok(BsFull { + call: s - k_disc, + put: 0, + call_delta: SCALE_I, + put_delta: 0, + gamma: 0, + vega: 0, + call_theta: -r_kd, + put_theta: 0, + call_rho: t_kd, + put_rho: 0, + }); + } + return Ok(BsFull { + call: 0, + put: k_disc - s, + call_delta: 0, + put_delta: -SCALE_I, + gamma: 0, + vega: 0, + call_theta: 0, + put_theta: r_kd, + call_rho: 0, + put_rho: -t_kd, + }); } - bs_full_hp(s, k, r, sigma, t) + let greeks = bs_full_hp(s, k, r, sigma, t)?; + validate_sabr_local(s, k, r, t, alpha, beta, rho, nu, (greeks.call, greeks.put))?; + Ok(greeks) } // ============================================================ @@ -200,46 +853,64 @@ pub fn sabr_greeks( /// /// Created by `sabr_precompute`. Passed to `sabr_vol_at` for each strike. /// All F-dependent quantities are cached: ln(F), F^(1-β), h numerators, ATM vol. -/// Per-strike cost drops from ~40K to ~25K CU (general β) or ~20K (β=0,1). +/// Final SBF audit: precompute averaged 31,633 CU (49,817 max) and each +/// `sabr_vol_at` averaged 29,609 CU (37,520 max). #[derive(Clone, Copy)] pub struct SabrSmile { f: u128, t: u128, ln_f: i128, one_minus_beta: u128, - half_omb: i128, // (1-β)/2 at SCALE - half_omb_ln_f: i128, // (1-β)/2 · ln(F) — for exp-based f_mid_pow + half_omb: i128, // (1-β)/2 at SCALE + half_omb_ln_f: i128, // (1-β)/2 · ln(F) — for exp-based f_mid_pow omb2: u128, omb4: u128, alpha_i: i128, rho: i128, nu_over_alpha: i128, - h1_num: i128, // (1-β)²·α² — h1 numerator - h2_num: i128, // ρ·β·ν·α — h2 numerator - h3: i128, // (2-3ρ²)·ν²/24 — strike-independent - atm_threshold: u128, + h1_num: i128, // (1-β)²·α² — h1 numerator + h2_num: i128, // ρ·β·ν·α — h2 numerator + h3: i128, // (2-3ρ²)·ν²/24 — strike-independent atm_vol: u128, } /// Precompute F-dependent SABR intermediates for batch smile pricing. /// -/// Cost: ~31K CU (general β), ~8K (β=0 or β=1). -/// Then call `sabr_vol_at` per strike at ~25K CU each. +/// Final SBF audit: 31,633 CU average / 49,817 max. Then call +/// `sabr_vol_at`, measured at 29,609 average / 37,520 max per strike. pub fn sabr_precompute( - f: u128, t: u128, - alpha: u128, beta: u128, rho: i128, nu: u128, + f: u128, + t: u128, + alpha: u128, + beta: u128, + rho: i128, + nu: u128, ) -> Result { - if f > i128::MAX as u128 || t > i128::MAX as u128 || alpha > i128::MAX as u128 - || beta > i128::MAX as u128 || nu > i128::MAX as u128 + if f > i128::MAX as u128 + || t > i128::MAX as u128 + || alpha > i128::MAX as u128 + || beta > i128::MAX as u128 + || nu > i128::MAX as u128 { return Err(SolMathError::Overflow); } if f == 0 || t == 0 || alpha == 0 { return Ok(SabrSmile { - f: 0, t: 0, ln_f: 0, one_minus_beta: 0, half_omb: 0, - half_omb_ln_f: 0, omb2: 0, omb4: 0, alpha_i: 0, rho: 0, - nu_over_alpha: 0, h1_num: 0, h2_num: 0, h3: 0, - atm_threshold: 0, atm_vol: 0, + f: 0, + t: 0, + ln_f: 0, + one_minus_beta: 0, + half_omb: 0, + half_omb_ln_f: 0, + omb2: 0, + omb4: 0, + alpha_i: 0, + rho: 0, + nu_over_alpha: 0, + h1_num: 0, + h2_num: 0, + h3: 0, + atm_vol: 0, }); } if beta > SCALE { @@ -248,7 +919,6 @@ pub fn sabr_precompute( if rho <= -SCALE_I || rho >= SCALE_I { return Err(SolMathError::DomainError); } - let s = SCALE; let si = SCALE_I; // s = SCALE = 1e12, beta ∈ [0, SCALE]; one_minus_beta ∈ [0, SCALE]. No underflow (u128). @@ -279,29 +949,40 @@ pub fn sabr_precompute( let h1_num = fp_mul_i(omb2 as i128, alpha2 as i128)?; let h2_num = fp_mul_i(fp_mul_i(rho, beta_i)?, fp_mul_i(nu_i, alpha_i)?)?; // 2 * si - 3 * rho2: si = SCALE_I = 1e12, rho2 ∈ [0, SCALE_I]; result ∈ [-1e12, 2e12]. Fits i128. - let h3 = fp_div_i( - fp_mul_i(2 * si - 3 * rho2, fp_mul_i(nu_i, nu_i)?)?, - 24 * si, - )?; + let h3 = fp_div_i(fp_mul_i(2 * si - 3 * rho2, fp_mul_i(nu_i, nu_i)?)?, 24 * si)?; // ATM vol - let atm_vol = if nu == 0 { - if atm_base_vol > 0 { atm_base_vol as u128 } else { 0 } - } else { + let atm_vol = { let f_pow_2 = fp_mul(f_pow, f_pow)?; - let h1 = if f_pow_2 == 0 { 0 } else { - fp_div_i(h1_num, 24 * f_pow_2 as i128)? + let h1 = if f_pow_2 == 0 { + 0 + } else { + fp_div_i(h1_num, checked_divisor(24, f_pow_2)?)? }; - let h2 = if f_pow == 0 { 0 } else { - fp_div_i(h2_num, 4 * f_pow as i128)? + let h2 = if f_pow == 0 { + 0 + } else { + fp_div_i(h2_num, checked_divisor(4, f_pow)?)? }; // h1, h2, h3 each ∈ [-SCALE_I, SCALE_I] by design; sum ∈ [-3·SCALE_I, 3·SCALE_I]. Fits i128. - let h = h1 + h2 + h3; + let h = h1 + .checked_add(h2) + .and_then(|v| v.checked_add(h3)) + .ok_or(SolMathError::Overflow)?; // h ∈ [-SCALE_I, SCALE_I]; fp_mul_i(h, t) ≤ SCALE_I for t ≤ SCALE_I; // si + correction ≤ 2·SCALE_I. Fits i128. - let correction = si + fp_mul_i(h, t as i128)?; + let correction = si + .checked_add(fp_mul_i(h, t as i128)?) + .ok_or(SolMathError::Overflow)?; + if correction <= 0 { + return Err(SolMathError::NoConvergence); + } let sigma_i = fp_mul_i(atm_base_vol, correction)?; - if sigma_i > 0 { sigma_i as u128 } else { 0 } + if sigma_i > 0 { + sigma_i as u128 + } else { + return Err(SolMathError::NoConvergence); + } }; // For exp-based f_mid_pow: f_mid^(1-β) = exp((1-β)/2 · (ln F + ln K)) @@ -311,17 +992,27 @@ pub fn sabr_precompute( let nu_over_alpha = if nu == 0 { 0 } else { fp_div_i(nu_i, alpha_i)? }; Ok(SabrSmile { - f, t, ln_f, one_minus_beta, half_omb, half_omb_ln_f, - omb2, omb4, alpha_i, rho, nu_over_alpha, - h1_num, h2_num, h3, - atm_threshold: f / 10_000, + f, + t, + ln_f, + one_minus_beta, + half_omb, + half_omb_ln_f, + omb2, + omb4, + alpha_i, + rho, + nu_over_alpha, + h1_num, + h2_num, + h3, atm_vol, }) } /// Compute SABR implied vol for a single strike using precomputed intermediates. /// -/// Cost: ~25K CU (general β), ~20K (β=0 or β=1). +/// Final SBF audit: 29,609 CU average, 33,215 median, 37,520 max. /// Uses exp((1-β)/2·(lnF+lnK)) for f_mid^(1-β) instead of pow_fixed_hp. pub fn sabr_vol_at(pre: &SabrSmile, k: u128) -> Result { if k == 0 || pre.alpha_i == 0 { @@ -331,8 +1022,7 @@ pub fn sabr_vol_at(pre: &SabrSmile, k: u128) -> Result { let si = SCALE_I; // ATM → cached - let is_atm = if pre.f > k { pre.f - k < pre.atm_threshold } - else { k - pre.f < pre.atm_threshold }; + let is_atm = pre.f == k; if is_atm { return Ok(pre.atm_vol); } @@ -349,15 +1039,22 @@ pub fn sabr_vol_at(pre: &SabrSmile, k: u128) -> Result { fp_sqrt(fp_mul(pre.f, k)?)? } else { // half_omb_ln_f and fp_mul_i(half_omb, ln_k) each ≤ 40·SCALE_I; sum ≤ 80·SCALE_I. Fits i128. - let exponent = pre.half_omb_ln_f + fp_mul_i(pre.half_omb, ln_k)?; + let exponent = pre + .half_omb_ln_f + .checked_add(fp_mul_i(pre.half_omb, ln_k)?) + .ok_or(SolMathError::Overflow)?; exp_fixed_i(exponent)? as u128 }; // D_log: si = 1e12; each corrective term ≤ si/24 < 1; d_log ∈ (SCALE_I, 2·SCALE_I). Fits i128. let log_fk_sq = fp_mul_i(log_fk, log_fk)?; + let d2 = fp_div_i(fp_mul_i(pre.omb2 as i128, log_fk_sq)?, 24 * si)?; + let log_fk_4 = fp_mul_i(log_fk_sq, log_fk_sq)?; + let d4 = fp_div_i(fp_mul_i(pre.omb4 as i128, log_fk_4)?, 1920 * si)?; let d_log = si - + fp_div_i(fp_mul_i(pre.omb2 as i128, log_fk_sq)?, 24 * si)? - + fp_div_i(fp_mul_i(pre.omb4 as i128, fp_mul_i(log_fk_sq, log_fk_sq)?)?, 1920 * si)?; + .checked_add(d2) + .and_then(|v| v.checked_add(d4)) + .ok_or(SolMathError::Overflow)?; // z let z = fp_mul_i(pre.nu_over_alpha, fp_mul_i(f_mid_pow as i128, log_fk)?)?; @@ -366,23 +1063,39 @@ pub fn sabr_vol_at(pre: &SabrSmile, k: u128) -> Result { // h using precomputed numerators let f_mid_pow_2 = fp_mul(f_mid_pow, f_mid_pow)?; - let h1 = if f_mid_pow_2 == 0 { 0 } else { - fp_div_i(pre.h1_num, 24 * f_mid_pow_2 as i128)? + let h1 = if f_mid_pow_2 == 0 { + 0 + } else { + fp_div_i(pre.h1_num, checked_divisor(24, f_mid_pow_2)?)? }; - let h2 = if f_mid_pow == 0 { 0 } else { - fp_div_i(pre.h2_num, 4 * f_mid_pow as i128)? + let h2 = if f_mid_pow == 0 { + 0 + } else { + fp_div_i(pre.h2_num, checked_divisor(4, f_mid_pow)?)? }; // h1, h2, pre.h3 each ∈ [-SCALE_I, SCALE_I] by design; sum ∈ [-3·SCALE_I, 3·SCALE_I]. Fits i128. - let h = h1 + h2 + pre.h3; + let h = h1 + .checked_add(h2) + .and_then(|v| v.checked_add(pre.h3)) + .ok_or(SolMathError::Overflow)?; // h ∈ [-SCALE_I, SCALE_I]; fp_mul_i(h, t) ≤ SCALE_I for t ≤ SCALE_I; // si + correction ≤ 2·SCALE_I. Fits i128. - let time_correction = si + fp_mul_i(h, pre.t as i128)?; + let time_correction = si + .checked_add(fp_mul_i(h, pre.t as i128)?) + .ok_or(SolMathError::Overflow)?; + if time_correction <= 0 { + return Err(SolMathError::NoConvergence); + } let denom = fp_mul_i(f_mid_pow as i128, d_log)?; let base_vol = fp_div_i(pre.alpha_i, denom)?; let sigma_i = fp_mul_i(fp_mul_i(base_vol, z_over_chi)?, time_correction)?; - Ok(if sigma_i > 0 { sigma_i as u128 } else { 0 }) + if sigma_i > 0 { + Ok(sigma_i as u128) + } else { + Err(SolMathError::NoConvergence) + } } // ============================================================ @@ -393,9 +1106,15 @@ pub fn sabr_vol_at(pre: &SabrSmile, k: u128) -> Result { /// Shared between sabr_implied_vol (single) paths. #[inline] fn sabr_assemble( - f_mid_pow: u128, log_fk: i128, - one_minus_beta: u128, alpha: u128, alpha_i: i128, beta_i: i128, - rho: i128, nu_i: i128, t: u128, + f_mid_pow: u128, + log_fk: i128, + one_minus_beta: u128, + alpha: u128, + alpha_i: i128, + beta_i: i128, + rho: i128, + nu_i: i128, + t: u128, ) -> Result { let si = SCALE_I; @@ -405,9 +1124,12 @@ fn sabr_assemble( let log_fk_4 = fp_mul_i(log_fk_sq, log_fk_sq)?; // d_log: si = 1e12; each corrective term ≤ si/24 < 1; sum ∈ (SCALE_I, 2·SCALE_I). Fits i128. + let d2 = fp_div_i(fp_mul_i(omb2 as i128, log_fk_sq)?, 24 * si)?; + let d4 = fp_div_i(fp_mul_i(omb4 as i128, log_fk_4)?, 1920 * si)?; let d_log = si - + fp_div_i(fp_mul_i(omb2 as i128, log_fk_sq)?, 24 * si)? - + fp_div_i(fp_mul_i(omb4 as i128, log_fk_4)?, 1920 * si)?; + .checked_add(d2) + .and_then(|v| v.checked_add(d4)) + .ok_or(SolMathError::Overflow)?; let z = fp_mul_i( fp_div_i(nu_i, alpha_i)?, @@ -419,20 +1141,34 @@ fn sabr_assemble( let h = compute_h(one_minus_beta, alpha, beta_i, rho, nu_i, alpha_i, f_mid_pow)?; // h ∈ [-SCALE_I, SCALE_I] by design; fp_mul_i(h, t) ≤ SCALE_I for t ≤ SCALE_I; // si + correction ≤ 2·SCALE_I. Fits i128. - let time_correction = si + fp_mul_i(h, t as i128)?; + let time_correction = si + .checked_add(fp_mul_i(h, t as i128)?) + .ok_or(SolMathError::Overflow)?; + if time_correction <= 0 { + return Err(SolMathError::NoConvergence); + } let denom = fp_mul_i(f_mid_pow as i128, d_log)?; let base_vol = fp_div_i(alpha_i, denom)?; let sigma_i = fp_mul_i(fp_mul_i(base_vol, z_over_chi)?, time_correction)?; - Ok(if sigma_i > 0 { sigma_i as u128 } else { 0 }) + if sigma_i > 0 { + Ok(sigma_i as u128) + } else { + Err(SolMathError::NoConvergence) + } } /// h = (1−β)²α²/(24·fpow²) + ρβνα/(4·fpow) + (2−3ρ²)ν²/24 #[inline] fn compute_h( - one_minus_beta: u128, alpha: u128, beta_i: i128, - rho: i128, nu_i: i128, alpha_i: i128, f_pow: u128, + one_minus_beta: u128, + alpha: u128, + beta_i: i128, + rho: i128, + nu_i: i128, + alpha_i: i128, + f_pow: u128, ) -> Result { let si = SCALE_I; let f_pow_2 = fp_mul(f_pow, f_pow)?; @@ -440,35 +1176,60 @@ fn compute_h( let alpha2 = fp_mul(alpha, alpha)?; let rho2 = fp_mul_i_fast(rho, rho); - let h1 = if f_pow_2 == 0 { 0 } else { - fp_div_i(fp_mul_i(omb2 as i128, alpha2 as i128)?, 24 * f_pow_2 as i128)? + let h1 = if f_pow_2 == 0 { + 0 + } else { + fp_div_i( + fp_mul_i(omb2 as i128, alpha2 as i128)?, + checked_divisor(24, f_pow_2)?, + )? }; - let h2 = if f_pow == 0 { 0 } else { + let h2 = if f_pow == 0 { + 0 + } else { fp_div_i( fp_mul_i(fp_mul_i_fast(rho, beta_i), fp_mul_i(nu_i, alpha_i)?)?, - 4 * f_pow as i128, + checked_divisor(4, f_pow)?, )? }; // 2 * si - 3 * rho2: si = SCALE_I = 1e12, rho2 ∈ [0, SCALE_I]; result ∈ [-1e12, 2e12]. Fits i128. - let h3 = fp_div_i( - fp_mul_i(2 * si - 3 * rho2, fp_mul_i(nu_i, nu_i)?)?, - 24 * si, - )?; + let h3 = fp_div_i(fp_mul_i(2 * si - 3 * rho2, fp_mul_i(nu_i, nu_i)?)?, 24 * si)?; // h1, h2, h3 each ∈ [-SCALE_I, SCALE_I] by design; sum ∈ [-3·SCALE_I, 3·SCALE_I]. Fits i128. - Ok(h1 + h2 + h3) + h1.checked_add(h2) + .and_then(|v| v.checked_add(h3)) + .ok_or(SolMathError::Overflow) +} + +#[inline] +fn checked_divisor(m: i128, x: u128) -> Result { + i128::try_from(x) + .ok() + .and_then(|v| v.checked_mul(m)) + .ok_or(SolMathError::Overflow) } /// z/χ(z) — exact via sqrt + ln. ~10K CU. fn sabr_z_over_chi(z: i128, rho: i128) -> Result { let si = SCALE_I; - if z.abs() < si / 1_000_000 { + if rho <= -si || rho >= si { + return Err(SolMathError::DomainError); + } + + if z.unsigned_abs() < (si / 1_000_000) as u128 { return Ok(si); } // si = 1e12; 2 * fp_mul_i(rho, z) ≤ 2·SCALE_I (both rho, z ≤ SCALE_I); disc ∈ (-1e12, 3e12). Fits i128. - let disc = si - 2 * fp_mul_i(rho, z)? + fp_mul_i(z, z)?; + let two_rho_z = fp_mul_i(rho, z)? + .checked_mul(2) + .ok_or(SolMathError::Overflow)?; + let z_sq = fp_mul_i(z, z)?; + let disc = si + .checked_sub(two_rho_z) + .and_then(|v| v.checked_add(z_sq)) + .ok_or(SolMathError::Overflow)?; let sqrt_disc = if disc > 0 { fp_sqrt(disc as u128)? as i128 @@ -477,17 +1238,30 @@ fn sabr_z_over_chi(z: i128, rho: i128) -> Result { }; // sqrt_disc ≤ SCALE_I, z ≤ SCALE_I, rho ∈ (-SCALE_I, SCALE_I); num ≤ 3·SCALE_I. Fits i128. - let num = sqrt_disc + z - rho; + let num = sqrt_disc + .checked_add(z) + .and_then(|v| v.checked_sub(rho)) + .ok_or(SolMathError::Overflow)?; // rho ∈ (-SCALE_I, SCALE_I); den = si - rho ∈ (0, 2·SCALE_I). Fits i128. let den = si - rho; - if den.abs() < si / 1000 { - return Ok(si); - } - - let ratio = fp_div_i(num, den)?; + let ratio = if num.unsigned_abs() < (si / 1000) as u128 || den < si / 1000 { + // Rationalized form avoids subtracting two nearly equal quantities: + // (sqrt+z-rho)/(1-rho) = (1+rho)/(sqrt-z+rho). + let alt_num = si.checked_add(rho).ok_or(SolMathError::Overflow)?; + let alt_den = sqrt_disc + .checked_sub(z) + .and_then(|v| v.checked_add(rho)) + .ok_or(SolMathError::Overflow)?; + if alt_den <= 0 { + return Err(SolMathError::NoConvergence); + } + fp_div_i(alt_num, alt_den)? + } else { + fp_div_i(num, den)? + }; if ratio <= 0 { - return Ok(si); + return Err(SolMathError::NoConvergence); } let chi = ln_fixed_i(ratio as u128)?; @@ -520,16 +1294,26 @@ fn sabr_z_over_chi(z: i128, rho: i128) -> Result { /// - |z| > 0.5: falls back to exact (sqrt + ln) /// /// # CU Cost -/// ~1K CU (polynomial path), ~10K CU (exact fallback). +/// Final mixed-path SBF audit: 10,911 CU average, 11,753 median, 12,855 max. pub fn sabr_z_over_chi_pade(z: i128, rho: i128) -> Result { let si = SCALE_I; - if z.abs() < si / 1_000_000 { + if rho <= -si || rho >= si { + return Err(SolMathError::DomainError); + } + + if z.unsigned_abs() < (si / 1_000_000) as u128 { return Ok(si); } + // The polynomial's error grows sharply as |rho| approaches one; use the + // stable exact/rationalized path throughout that region. + if rho.unsigned_abs() > 900_000_000_000 { + return sabr_z_over_chi(z, rho); + } + // Fall back to exact for |z| > 0.5 - if z.abs() > si / 2 { + if z.unsigned_abs() > (si / 2) as u128 { return sabr_z_over_chi(z, rho); } @@ -557,6 +1341,672 @@ pub fn sabr_z_over_chi_pade(z: i128, rho: i128) -> Result { mod tests { use super::*; + const SURFACE_STRIKES: [u128; 3] = [80 * SCALE, 100 * SCALE, 120 * SCALE]; + const SURFACE_MATURITIES: [u128; 2] = [SCALE, 2 * SCALE]; + const SURFACE_CALLS: [u128; 6] = [ + 25 * SCALE, + 12 * SCALE, + 5 * SCALE, + 27 * SCALE, + 15 * SCALE, + 8 * SCALE, + ]; + const SURFACE_PUTS: [u128; 6] = [ + 5 * SCALE, + 12 * SCALE, + 25 * SCALE, + 7 * SCALE, + 15 * SCALE, + 28 * SCALE, + ]; + + fn fill_zero_rate_intrinsic_surface( + spot: u128, + strikes: &[u128], + maturities: &[u128], + calls: &mut [u128], + puts: &mut [u128], + ) { + assert_eq!(calls.len(), strikes.len() * maturities.len()); + assert_eq!(puts.len(), calls.len()); + let mut maturity_index = 0usize; + while maturity_index < maturities.len() { + let mut strike_index = 0usize; + while strike_index < strikes.len() { + let index = maturity_index * strikes.len() + strike_index; + calls[index] = spot.saturating_sub(strikes[strike_index]); + puts[index] = strikes[strike_index].saturating_sub(spot); + strike_index += 1; + } + maturity_index += 1; + } + } + + #[test] + fn certified_surface_returns_stored_quotes_without_repricing() { + let certificate = certify_sabr_surface( + 100 * SCALE, + 0, + &SURFACE_STRIKES, + &SURFACE_MATURITIES, + &SURFACE_CALLS, + &SURFACE_PUTS, + ) + .unwrap(); + + assert_eq!(certificate.spot(), 100 * SCALE); + assert_eq!(certificate.rate(), 0); + assert_eq!(certificate.strikes(), &SURFACE_STRIKES); + assert_eq!(certificate.maturities(), &SURFACE_MATURITIES); + assert_eq!(certificate.quote_count(), 6); + let quote = certificate.quote_at(1, 2).unwrap(); + assert_eq!(quote.spot(), 100 * SCALE); + assert_eq!(quote.rate(), 0); + assert_eq!(quote.strike(), 120 * SCALE); + assert_eq!(quote.maturity(), 2 * SCALE); + assert_eq!(quote.call(), 8 * SCALE); + assert_eq!(quote.put(), 28 * SCALE); + assert_eq!(certificate.quote_at(2, 0), Err(SolMathError::DomainError)); + assert_eq!(certificate.quote_at(0, 3), Err(SolMathError::DomainError)); + } + + #[test] + fn certified_surface_rejects_malformed_axes_and_shapes() { + assert_eq!( + certify_sabr_surface(100 * SCALE, 0, &[], &SURFACE_MATURITIES, &[], &[]), + Err(SolMathError::DomainError) + ); + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + 0, + &SURFACE_STRIKES[..2], + &SURFACE_MATURITIES, + &SURFACE_CALLS[..4], + &SURFACE_PUTS[..4], + ), + Err(SolMathError::DomainError) + ); + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + 0, + &SURFACE_STRIKES, + &SURFACE_MATURITIES, + &SURFACE_CALLS[..5], + &SURFACE_PUTS, + ), + Err(SolMathError::DomainError) + ); + + let duplicate_strikes = [80 * SCALE, 80 * SCALE, 120 * SCALE]; + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + 0, + &duplicate_strikes, + &SURFACE_MATURITIES, + &SURFACE_CALLS, + &SURFACE_PUTS, + ), + Err(SolMathError::DomainError) + ); + let descending_maturities = [2 * SCALE, SCALE]; + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + 0, + &SURFACE_STRIKES, + &descending_maturities, + &SURFACE_CALLS, + &SURFACE_PUTS, + ), + Err(SolMathError::DomainError) + ); + let zero_strike = [0, 100 * SCALE, 120 * SCALE]; + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + 0, + &zero_strike, + &SURFACE_MATURITIES, + &SURFACE_CALLS, + &SURFACE_PUTS, + ), + Err(SolMathError::DomainError) + ); + } + + #[test] + fn certified_surface_requires_exact_parity_and_bounds() { + let mut bad_puts = SURFACE_PUTS; + bad_puts[4] += 1; + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + 0, + &SURFACE_STRIKES, + &SURFACE_MATURITIES, + &SURFACE_CALLS, + &bad_puts, + ), + Err(SolMathError::NoConvergence) + ); + + let mut calls = SURFACE_CALLS; + let mut puts = SURFACE_PUTS; + calls[0] = 101 * SCALE; + puts[0] = 81 * SCALE; // preserves parity at K=80 but violates both bounds + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + 0, + &SURFACE_STRIKES, + &SURFACE_MATURITIES, + &calls, + &puts, + ), + Err(SolMathError::NoConvergence) + ); + } + + #[test] + fn certified_surface_uses_discounted_strike_at_positive_rates() { + let spot = 100 * SCALE; + let rate = 50_000_000_000; // 5% + + // Keep every discounted strike below spot so the zero-volatility call + // curve is the same linear function C(x) = S - x at both maturities. + // A grid that straddles the intrinsic kink at x = S has a different + // piecewise-linear interpolant after each maturity's discount shifts + // the nominal strike axis, so it is not a valid calendar fixture. + let strikes = [60 * SCALE, 80 * SCALE, 100 * SCALE]; + let mut calls = [0u128; 6]; + let mut puts = [0u128; 6]; + + let mut maturity_index = 0usize; + while maturity_index < SURFACE_MATURITIES.len() { + let discount = + sabr_surface_discount(rate, SURFACE_MATURITIES[maturity_index]).unwrap() as u128; + let mut strike_index = 0usize; + while strike_index < strikes.len() { + let index = maturity_index * strikes.len() + strike_index; + let discounted_strike = fp_mul(strikes[strike_index], discount).unwrap(); + calls[index] = spot.saturating_sub(discounted_strike); + puts[index] = discounted_strike.saturating_sub(spot); + strike_index += 1; + } + maturity_index += 1; + } + + let first_discount = sabr_surface_discount(rate, SURFACE_MATURITIES[0]).unwrap(); + let second_discount = sabr_surface_discount(rate, SURFACE_MATURITIES[1]).unwrap(); + let mut first_axis = [0u128; MAX_SABR_SURFACE_STRIKES]; + let mut second_axis = [0u128; MAX_SABR_SURFACE_STRIKES]; + fill_discounted_strikes(&strikes, first_discount, &mut first_axis).unwrap(); + fill_discounted_strikes(&strikes, second_discount, &mut second_axis).unwrap(); + assert_eq!( + validate_sabr_surface_row( + spot, + &first_axis[..strikes.len()], + &calls[..strikes.len()], + &puts[..strikes.len()], + ), + Ok(()) + ); + assert_eq!( + validate_sabr_surface_row( + spot, + &second_axis[..strikes.len()], + &calls[strikes.len()..], + &puts[strikes.len()..], + ), + Ok(()) + ); + assert_eq!( + validate_sabr_surface_calendar( + spot, + &strikes, + first_discount, + &calls[..strikes.len()], + second_discount, + &calls[strikes.len()..], + ), + Ok(()) + ); + + let certificate = + certify_sabr_surface(spot, rate, &strikes, &SURFACE_MATURITIES, &calls, &puts).unwrap(); + assert_eq!(certificate.quote_at(0, 0).unwrap().put(), 0); + assert!(certificate.quote_at(1, 0).unwrap().call() > calls[0]); + } + + #[test] + fn distant_wing_individually_valid_quotes_fail_global_butterfly() { + let strikes = [50 * SCALE, 100 * SCALE, 200 * SCALE]; + let maturities = [SCALE, 2 * SCALE]; + let calls = [55 * SCALE, 54 * SCALE, SCALE, 55 * SCALE, 54 * SCALE, SCALE]; + let puts = [ + 5 * SCALE, + 54 * SCALE, + 101 * SCALE, + 5 * SCALE, + 54 * SCALE, + 101 * SCALE, + ]; + + // Every isolated quote satisfies hard bounds and exact parity. Only + // the distant, irregularly-spaced butterfly reveals the arbitrage. + let mut row = 0usize; + while row < maturities.len() { + let mut column = 0usize; + while column < strikes.len() { + let index = row * strikes.len() + column; + assert_eq!( + validate_sabr_surface_quote( + 100 * SCALE, + strikes[column], + calls[index], + puts[index], + ), + Ok(()) + ); + column += 1; + } + row += 1; + } + assert_eq!( + certify_sabr_surface(100 * SCALE, 0, &strikes, &maturities, &calls, &puts,), + Err(SolMathError::NoConvergence) + ); + } + + #[test] + fn distant_wing_local_sabr_guards_pass_but_global_surface_fails() { + let strikes = [50 * SCALE, 100 * SCALE, 200 * SCALE]; + let maturities = [SCALE, 2 * SCALE]; + // Each strike uses a locally plausible calibration. The extreme wing + // has much higher alpha, which a single-strike/local-neighbour guard + // cannot compare with the rest of the assembled quote surface. + let alphas = [100_000_000_000, 100_000_000_000, 2 * SCALE]; + let mut calls = [0u128; 6]; + let mut puts = [0u128; 6]; + + let mut maturity_index = 0usize; + while maturity_index < maturities.len() { + let mut strike_index = 0usize; + while strike_index < strikes.len() { + let index = maturity_index * strikes.len() + strike_index; + let (call, put) = sabr_price( + 100 * SCALE, + strikes[strike_index], + 0, + maturities[maturity_index], + alphas[strike_index], + SCALE, + 0, + 0, + ) + .expect("each isolated quote must pass the local SABR guard"); + calls[index] = call; + puts[index] = put; + strike_index += 1; + } + maturity_index += 1; + } + + assert!( + calls[2] > calls[1], + "the distant call wing rises with strike" + ); + assert_eq!( + certify_sabr_surface(100 * SCALE, 0, &strikes, &maturities, &calls, &puts,), + Err(SolMathError::NoConvergence) + ); + } + + #[test] + fn certified_surface_rejects_vertical_spread_and_calendar_arbitrage() { + let strikes = [100 * SCALE, 101 * SCALE, 102 * SCALE]; + let maturities = [SCALE, 2 * SCALE]; + let calls = [100 * SCALE, 0, 0, 100 * SCALE, 0, 0]; + let puts = [100 * SCALE, SCALE, 2 * SCALE, 100 * SCALE, SCALE, 2 * SCALE]; + assert_eq!( + certify_sabr_surface(100 * SCALE, 0, &strikes, &maturities, &calls, &puts,), + Err(SolMathError::NoConvergence) + ); + + let mut calendar_calls = SURFACE_CALLS; + let mut calendar_puts = SURFACE_PUTS; + calendar_calls[3] = 24 * SCALE; + calendar_puts[3] = 4 * SCALE; + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + 0, + &SURFACE_STRIKES, + &SURFACE_MATURITIES, + &calendar_calls, + &calendar_puts, + ), + Err(SolMathError::NoConvergence) + ); + } + + #[test] + fn certified_surface_unequal_spacing_uses_correct_slope_direction() { + let strikes = [80 * SCALE, 100 * SCALE, 140 * SCALE]; + let maturities = [SCALE, 2 * SCALE]; + let calls = [ + 30 * SCALE, + 20 * SCALE, + 10 * SCALE, + 30 * SCALE, + 20 * SCALE, + 10 * SCALE, + ]; + let puts = [ + 10 * SCALE, + 20 * SCALE, + 50 * SCALE, + 10 * SCALE, + 20 * SCALE, + 50 * SCALE, + ]; + assert!( + certify_sabr_surface(100 * SCALE, 0, &strikes, &maturities, &calls, &puts,).is_ok() + ); + + // The middle quote reverses the required slope ordering: the call + // falls faster, and the put rises slower, in the wider right interval. + let concave_calls = [ + 30 * SCALE, + 25 * SCALE, + 10 * SCALE, + 30 * SCALE, + 25 * SCALE, + 10 * SCALE, + ]; + let concave_puts = [ + 10 * SCALE, + 25 * SCALE, + 50 * SCALE, + 10 * SCALE, + 25 * SCALE, + 50 * SCALE, + ]; + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + 0, + &strikes, + &maturities, + &concave_calls, + &concave_puts, + ), + Err(SolMathError::NoConvergence) + ); + } + + #[test] + fn certified_surface_wide_products_are_exact_near_i128_limit() { + let maximum = i128::MAX as u128; + let strikes = [1, maximum / 2, maximum]; + let maturities = [SCALE, 2 * SCALE]; + let row = [maximum - 1, maximum - maximum / 2, 0]; + let calls = [row[0], row[1], row[2], row[0], row[1], row[2]]; + let puts = [0u128; 6]; + + let left_drop = calls[0] - calls[1]; + let right_width = strikes[2] - strikes[1]; + assert!(left_drop.checked_mul(right_width).is_none()); + assert!(certify_sabr_surface(maximum, 0, &strikes, &maturities, &calls, &puts,).is_ok()); + + assert_eq!( + certify_sabr_surface( + maximum + 1, + 0, + &SURFACE_STRIKES, + &SURFACE_MATURITIES, + &SURFACE_CALLS, + &SURFACE_PUTS, + ), + Err(SolMathError::Overflow) + ); + let excessive_strike = [1, maximum, maximum + 1]; + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + 0, + &excessive_strike, + &SURFACE_MATURITIES, + &SURFACE_CALLS, + &SURFACE_PUTS, + ), + Err(SolMathError::Overflow) + ); + let excessive_maturity = [SCALE, maximum + 1]; + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + 0, + &SURFACE_STRIKES, + &excessive_maturity, + &SURFACE_CALLS, + &SURFACE_PUTS, + ), + Err(SolMathError::Overflow) + ); + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + maximum + 1, + &SURFACE_STRIKES, + &SURFACE_MATURITIES, + &SURFACE_CALLS, + &SURFACE_PUTS, + ), + Err(SolMathError::Overflow) + ); + } + + #[test] + fn certified_surface_enforces_bounded_grid_limits() { + let spot = 100 * SCALE; + let strikes_at_limit: [u128; MAX_SABR_SURFACE_STRIKES] = + core::array::from_fn(|i| (i as u128 + 1) * 5 * SCALE); + let maturities_for_strike_limit: [u128; 8] = + core::array::from_fn(|i| (i as u128 + 1) * SCALE); + let mut calls_at_limit = [0u128; MAX_SABR_SURFACE_QUOTES]; + let mut puts_at_limit = [0u128; MAX_SABR_SURFACE_QUOTES]; + fill_zero_rate_intrinsic_surface( + spot, + &strikes_at_limit, + &maturities_for_strike_limit, + &mut calls_at_limit, + &mut puts_at_limit, + ); + assert_eq!( + certify_sabr_surface( + spot, + 0, + &strikes_at_limit, + &maturities_for_strike_limit, + &calls_at_limit, + &puts_at_limit, + ) + .unwrap() + .quote_count(), + MAX_SABR_SURFACE_QUOTES + ); + + let strikes_for_maturity_limit: [u128; 16] = + core::array::from_fn(|i| (i as u128 + 1) * 10 * SCALE); + let maturities_at_limit: [u128; MAX_SABR_SURFACE_MATURITIES] = + core::array::from_fn(|i| (i as u128 + 1) * SCALE); + let mut maturity_limit_calls = [0u128; MAX_SABR_SURFACE_QUOTES]; + let mut maturity_limit_puts = [0u128; MAX_SABR_SURFACE_QUOTES]; + fill_zero_rate_intrinsic_surface( + spot, + &strikes_for_maturity_limit, + &maturities_at_limit, + &mut maturity_limit_calls, + &mut maturity_limit_puts, + ); + assert!(certify_sabr_surface( + spot, + 0, + &strikes_for_maturity_limit, + &maturities_at_limit, + &maturity_limit_calls, + &maturity_limit_puts, + ) + .is_ok()); + + let too_many_strikes: [u128; MAX_SABR_SURFACE_STRIKES + 1] = + core::array::from_fn(|i| (i as u128 + 1) * SCALE); + let two_maturities = [SCALE, 2 * SCALE]; + let oversized_strike_calls = [0u128; (MAX_SABR_SURFACE_STRIKES + 1) * 2]; + assert_eq!( + certify_sabr_surface( + spot, + 0, + &too_many_strikes, + &two_maturities, + &oversized_strike_calls, + &oversized_strike_calls, + ), + Err(SolMathError::DomainError) + ); + + let three_strikes = [80 * SCALE, 100 * SCALE, 120 * SCALE]; + let too_many_maturities: [u128; MAX_SABR_SURFACE_MATURITIES + 1] = + core::array::from_fn(|i| (i as u128 + 1) * SCALE); + let oversized_maturity_calls = [0u128; 3 * (MAX_SABR_SURFACE_MATURITIES + 1)]; + assert_eq!( + certify_sabr_surface( + spot, + 0, + &three_strikes, + &too_many_maturities, + &oversized_maturity_calls, + &oversized_maturity_calls, + ), + Err(SolMathError::DomainError) + ); + + let quote_cap_strikes: [u128; 17] = core::array::from_fn(|i| (i as u128 + 1) * SCALE); + let quote_cap_maturities: [u128; 16] = core::array::from_fn(|i| (i as u128 + 1) * SCALE); + let too_many_quotes = [0u128; 17 * 16]; + assert_eq!( + certify_sabr_surface( + spot, + 0, + "e_cap_strikes, + "e_cap_maturities, + &too_many_quotes, + &too_many_quotes, + ), + Err(SolMathError::DomainError) + ); + } + + #[test] + fn test_sabr_pade_rejects_invalid_rho_before_fast_path() { + assert_eq!( + sabr_z_over_chi_pade(0, SCALE_I), + Err(SolMathError::DomainError) + ); + assert_eq!( + sabr_z_over_chi_pade(0, i128::MIN), + Err(SolMathError::DomainError) + ); + } + + #[test] + fn test_sabr_exact_path_is_continuous_near_rho_one() { + let a = sabr_z_over_chi_pade(500_000_000_000, 999_900_000_000).unwrap(); + let b = sabr_z_over_chi_pade(500_000_000_001, 999_900_000_000).unwrap(); + assert!(a.abs_diff(b) < 10_000_000, "a={a}, b={b}"); + assert!((700_000_000_000..750_000_000_000).contains(&a)); + } + + #[test] + fn test_sabr_rejects_non_positive_asymptotic_correction() { + assert_eq!( + sabr_implied_vol( + 100 * SCALE, + 100 * SCALE, + 20 * SCALE, + 200_000_000_000, + SCALE, + 990_000_000_000, + 2 * SCALE, + ), + Err(SolMathError::NoConvergence) + ); + } + + #[test] + fn test_sabr_price_rejects_locally_increasing_call_wing() { + assert_eq!( + sabr_price( + 100 * SCALE, + 165 * SCALE, + 0, + 2 * SCALE, + SCALE / 2, + SCALE, + 900_000_000_000, + SCALE, + ), + Err(SolMathError::NoConvergence) + ); + } + + #[test] + fn test_sabr_rejects_uncertified_global_wing_regime() { + assert_eq!( + sabr_price( + 100 * SCALE, + 100 * SCALE, + 0, + 2 * SCALE, + SCALE, + SCALE, + 0, + 5 * SCALE, + ), + Err(SolMathError::NoConvergence) + ); + } + + #[test] + fn test_sabr_deterministic_greeks_are_not_zeroed() { + let g = sabr_greeks( + 110 * SCALE, + 100 * SCALE, + 50_000_000_000, + SCALE, + 0, + SCALE / 2, + 0, + 0, + ) + .unwrap(); + assert_eq!(g.call_delta, SCALE_I); + assert!(g.call_rho > 0); + assert!(g.call_theta < 0); + } + + #[test] + fn test_sabr_pade_minimum_z_returns_error_not_panic() { + assert_eq!( + sabr_z_over_chi_pade(i128::MIN, 0), + Err(SolMathError::Overflow) + ); + } + // --- Single-strike tests --- #[test] @@ -569,8 +2019,11 @@ mod tests { let t = SCALE; let vol = sabr_implied_vol(f, f, t, alpha, beta, rho, nu).unwrap(); - assert!(vol > 10_000_000_000 && vol < 50_000_000_000, - "ATM vol {} out of range", vol); + assert!( + vol > 10_000_000_000 && vol < 50_000_000_000, + "ATM vol {} out of range", + vol + ); } #[test] @@ -587,8 +2040,13 @@ mod tests { let vol_low = sabr_implied_vol(f, k_low, t, alpha, beta, rho, nu).unwrap(); let vol_high = sabr_implied_vol(f, k_high, t, alpha, beta, rho, nu).unwrap(); let diff = (vol_low as i128 - vol_high as i128).abs(); - assert!(diff < SCALE_I / 1000, - "Symmetry broken: low={} high={} diff={}", vol_low, vol_high, diff); + assert!( + diff < SCALE_I / 1000, + "Symmetry broken: low={} high={} diff={}", + vol_low, + vol_high, + diff + ); } #[test] @@ -602,7 +2060,12 @@ mod tests { let vol = sabr_implied_vol(f, k, t, alpha, beta, 0, 0).unwrap(); let expected = 20_000_000_000u128; let diff = (vol as i128 - expected as i128).abs(); - assert!(diff < SCALE_I / 100, "CEV vol={} expected≈{}", vol, expected); + assert!( + diff < SCALE_I / 100, + "CEV vol={} expected≈{}", + vol, + expected + ); } #[test] @@ -618,7 +2081,8 @@ mod tests { let (call, put) = sabr_price(s, k, r, t, alpha, beta, rho, nu).unwrap(); let disc = exp_fixed_i(-fp_mul_i(r as i128, t as i128).unwrap()).unwrap(); - let parity = (call as i128 - put as i128 - s as i128 + fp_mul_i(k as i128, disc).unwrap()).abs(); + let parity = + (call as i128 - put as i128 - s as i128 + fp_mul_i(k as i128, disc).unwrap()).abs(); assert!(parity < 1000, "Put-call parity error: {}", parity); } @@ -635,8 +2099,18 @@ mod tests { let vol_100 = sabr_implied_vol(f, f, t, alpha, beta, rho, nu).unwrap(); let vol_110 = sabr_implied_vol(f, 110 * SCALE, t, alpha, beta, rho, nu).unwrap(); - assert!(vol_90 > vol_100, "Expected vol_90 > vol_100: {} vs {}", vol_90, vol_100); - assert!(vol_90 > vol_110, "Expected vol_90 > vol_110 (skew): {} vs {}", vol_90, vol_110); + assert!( + vol_90 > vol_100, + "Expected vol_90 > vol_100: {} vs {}", + vol_90, + vol_100 + ); + assert!( + vol_90 > vol_110, + "Expected vol_90 > vol_110 (skew): {} vs {}", + vol_90, + vol_110 + ); } #[test] @@ -652,8 +2126,18 @@ mod tests { let vol_100 = sabr_implied_vol(f, f, t, alpha, beta, rho, nu).unwrap(); let vol_110 = sabr_implied_vol(f, 110 * SCALE, t, alpha, beta, rho, nu).unwrap(); - assert!(vol_90 > vol_100, "Left wing below ATM: {} vs {}", vol_90, vol_100); - assert!(vol_110 > vol_100, "Right wing below ATM: {} vs {}", vol_110, vol_100); + assert!( + vol_90 > vol_100, + "Left wing below ATM: {} vs {}", + vol_90, + vol_100 + ); + assert!( + vol_110 > vol_100, + "Right wing below ATM: {} vs {}", + vol_110, + vol_100 + ); } #[test] @@ -670,16 +2154,33 @@ mod tests { let vol = sabr_implied_vol(f, k, t, alpha, beta, rho, nu); assert!(vol.is_ok(), "Failed at K={}bp: {:?}", k_bp, vol); let v = vol.unwrap(); - assert!(v > SCALE / 100 && v < 2 * SCALE, "Vol {} out of range at K={}bp", v, k_bp); + assert!( + v > SCALE / 100 && v < 2 * SCALE, + "Vol {} out of range at K={}bp", + v, + k_bp + ); } } #[test] fn test_sabr_zero_inputs() { - assert_eq!(sabr_implied_vol(0, SCALE, SCALE, SCALE, SCALE/2, 0, SCALE).unwrap(), 0); - assert_eq!(sabr_implied_vol(SCALE, 0, SCALE, SCALE, SCALE/2, 0, SCALE).unwrap(), 0); - assert_eq!(sabr_implied_vol(SCALE, SCALE, 0, SCALE, SCALE/2, 0, SCALE).unwrap(), 0); - assert_eq!(sabr_implied_vol(SCALE, SCALE, SCALE, 0, SCALE/2, 0, SCALE).unwrap(), 0); + assert_eq!( + sabr_implied_vol(0, SCALE, SCALE, SCALE, SCALE / 2, 0, SCALE).unwrap(), + 0 + ); + assert_eq!( + sabr_implied_vol(SCALE, 0, SCALE, SCALE, SCALE / 2, 0, SCALE).unwrap(), + 0 + ); + assert_eq!( + sabr_implied_vol(SCALE, SCALE, 0, SCALE, SCALE / 2, 0, SCALE).unwrap(), + 0 + ); + assert_eq!( + sabr_implied_vol(SCALE, SCALE, SCALE, 0, SCALE / 2, 0, SCALE).unwrap(), + 0 + ); } #[test] @@ -702,7 +2203,11 @@ mod tests { let t = SCALE; let vol_atm = sabr_implied_vol(f, f, t, alpha, SCALE, rho, nu).unwrap(); let vol_otm = sabr_implied_vol(f, 110 * SCALE, t, alpha, SCALE, rho, nu).unwrap(); - assert!(vol_atm > 150_000_000_000 && vol_atm < 300_000_000_000, "β=1 ATM vol {}", vol_atm); + assert!( + vol_atm > 150_000_000_000 && vol_atm < 300_000_000_000, + "β=1 ATM vol {}", + vol_atm + ); assert!(vol_otm > 0); } @@ -717,7 +2222,13 @@ mod tests { let vol_exact = sabr_implied_vol(f, k, t, alpha, SCALE, rho, nu).unwrap(); let vol_near = sabr_implied_vol(f, k, t, alpha, SCALE - 1, rho, nu).unwrap(); let diff = (vol_exact as i128 - vol_near as i128).abs(); - assert!(diff < 100, "β=1 limit: exact={} near={} diff={}", vol_exact, vol_near, diff); + assert!( + diff < 100, + "β=1 limit: exact={} near={} diff={}", + vol_exact, + vol_near, + diff + ); } #[test] @@ -751,17 +2262,36 @@ mod tests { let vol_single = sabr_implied_vol(f, f, t, alpha, beta, rho, nu).unwrap(); let vol_batch = sabr_vol_at(&pre, f).unwrap(); let atm_diff = (vol_single as i128 - vol_batch as i128).abs(); - assert!(atm_diff < 10, "ATM mismatch: single={} batch={}", vol_single, vol_batch); + assert!( + atm_diff < 10, + "ATM mismatch: single={} batch={}", + vol_single, + vol_batch + ); // OTM strikes — batch uses exp instead of pow, so allow ~5 ULP tolerance - for &k in &[85 * SCALE, 90 * SCALE, 95 * SCALE, 105 * SCALE, 110 * SCALE, 115 * SCALE] { + for &k in &[ + 85 * SCALE, + 90 * SCALE, + 95 * SCALE, + 105 * SCALE, + 110 * SCALE, + 115 * SCALE, + ] { let vol_s = sabr_implied_vol(f, k, t, alpha, beta, rho, nu).unwrap(); let vol_b = sabr_vol_at(&pre, k).unwrap(); let diff = (vol_s as i128 - vol_b as i128).abs(); // exp-based f_mid_pow may differ from pow_fixed_hp by a few ULP let tol = vol_s / 10_000; // 0.01% relative tolerance - assert!(diff < tol as i128 || diff < 10, - "K={}: single={} batch={} diff={} tol={}", k/SCALE, vol_s, vol_b, diff, tol); + assert!( + diff < tol as i128 || diff < 10, + "K={}: single={} batch={} diff={} tol={}", + k / SCALE, + vol_s, + vol_b, + diff, + tol + ); } } @@ -775,12 +2305,25 @@ mod tests { let pre = sabr_precompute(f, t, alpha, SCALE, rho, nu).unwrap(); - for &k in &[90 * SCALE, 95 * SCALE, 100 * SCALE, 105 * SCALE, 110 * SCALE] { + for &k in &[ + 90 * SCALE, + 95 * SCALE, + 100 * SCALE, + 105 * SCALE, + 110 * SCALE, + ] { let vol_s = sabr_implied_vol(f, k, t, alpha, SCALE, rho, nu).unwrap(); let vol_b = sabr_vol_at(&pre, k).unwrap(); let diff = (vol_s as i128 - vol_b as i128).abs(); // β=1: no pow/exp, should match closely - assert!(diff < 100, "β=1 K={}: single={} batch={} diff={}", k/SCALE, vol_s, vol_b, diff); + assert!( + diff < 100, + "β=1 K={}: single={} batch={} diff={}", + k / SCALE, + vol_s, + vol_b, + diff + ); } } @@ -799,13 +2342,20 @@ mod tests { let vol_b = sabr_vol_at(&pre, k).unwrap(); let diff = (vol_s as i128 - vol_b as i128).abs(); // β=0: both use sqrt, should match exactly - assert!(diff < 100, "β=0 K={}: single={} batch={} diff={}", k/SCALE, vol_s, vol_b, diff); + assert!( + diff < 100, + "β=0 K={}: single={} batch={} diff={}", + k / SCALE, + vol_s, + vol_b, + diff + ); } } #[test] fn test_sabr_smile_zero_inputs() { - let pre = sabr_precompute(0, SCALE, SCALE, SCALE/2, 0, SCALE).unwrap(); + let pre = sabr_precompute(0, SCALE, SCALE, SCALE / 2, 0, SCALE).unwrap(); assert_eq!(sabr_vol_at(&pre, SCALE).unwrap(), 0); } @@ -814,10 +2364,23 @@ mod tests { #[test] fn test_pade_vs_exact_small_z() { // Taylor-3 tested across ρ × z grid - let rhos = [-800_000_000_000i128, -500_000_000_000, -200_000_000_000, - 0, 200_000_000_000, 500_000_000_000, 800_000_000_000]; - let zs = [-400_000_000_000i128, -200_000_000_000, -100_000_000_000, - 100_000_000_000, 200_000_000_000, 400_000_000_000]; + let rhos = [ + -800_000_000_000i128, + -500_000_000_000, + -200_000_000_000, + 0, + 200_000_000_000, + 500_000_000_000, + 800_000_000_000, + ]; + let zs = [ + -400_000_000_000i128, + -200_000_000_000, + -100_000_000_000, + 100_000_000_000, + 200_000_000_000, + 400_000_000_000, + ]; let mut max_rel_err: i128 = 0; for &rho in &rhos { @@ -827,13 +2390,18 @@ mod tests { let err = (exact - approx).abs(); if exact.abs() > SCALE_I / 100 { let rel = err * 1_000_000 / exact.abs(); // ppm - if rel > max_rel_err { max_rel_err = rel; } + if rel > max_rel_err { + max_rel_err = rel; + } } } } // Taylor-3 within 2% for |z| < 0.5, degrades for |ρ| > 0.7 - assert!(max_rel_err < 20_000, - "Max relative error for |z|<0.5: {} ppm (expect <20000)", max_rel_err); + assert!( + max_rel_err < 20_000, + "Max relative error for |z|<0.5: {} ppm (expect <20000)", + max_rel_err + ); } #[test] @@ -854,13 +2422,37 @@ mod tests { fn test_sabr_put_call_parity_sweep() { let r = 50_000_000_000u128; let params: &[(u128, u128, u128, i128, u128)] = &[ - (100*SCALE, 200_000_000_000, SCALE/2, -300_000_000_000, 400_000_000_000), - (100*SCALE, 300_000_000_000, SCALE, -700_000_000_000, 500_000_000_000), - (100*SCALE, 200_000_000_000, 0, -300_000_000_000, 400_000_000_000), - (50*SCALE, 150_000_000_000, 700_000_000_000, -500_000_000_000, 300_000_000_000), + ( + 100 * SCALE, + 200_000_000_000, + SCALE / 2, + -300_000_000_000, + 400_000_000_000, + ), + ( + 100 * SCALE, + 300_000_000_000, + SCALE, + -700_000_000_000, + 500_000_000_000, + ), + ( + 100 * SCALE, + 200_000_000_000, + 0, + -300_000_000_000, + 400_000_000_000, + ), + ( + 50 * SCALE, + 150_000_000_000, + 700_000_000_000, + -500_000_000_000, + 300_000_000_000, + ), ]; let strikes: &[u128] = &[80, 90, 95, 100, 105, 110, 120]; - let mats: &[u128] = &[100_000_000_000, 250_000_000_000, SCALE, 2*SCALE]; + let mats: &[u128] = &[100_000_000_000, 250_000_000_000, SCALE, 2 * SCALE]; for &(s, alpha, beta, rho, nu) in params { for &k_m in strikes { @@ -868,10 +2460,17 @@ mod tests { for &t in mats { if let Ok((call, put)) = sabr_price(s, k, r, t, alpha, beta, rho, nu) { let disc = exp_fixed_i(-fp_mul_i(r as i128, t as i128).unwrap()).unwrap(); - let parity = (call as i128 - put as i128 - - s as i128 + fp_mul_i(k as i128, disc).unwrap()).abs(); - assert!(parity < 10_000, - "P/C parity: s={} k={} t={} err={}", s/SCALE, k/SCALE, t, parity); + let parity = (call as i128 - put as i128 - s as i128 + + fp_mul_i(k as i128, disc).unwrap()) + .abs(); + assert!( + parity < 10_000, + "P/C parity: s={} k={} t={} err={}", + s / SCALE, + k / SCALE, + t, + parity + ); } } } @@ -881,16 +2480,35 @@ mod tests { #[test] fn test_sabr_vol_positive_sweep() { let params: &[(u128, u128, u128, i128, u128)] = &[ - (100*SCALE, 200_000_000_000, SCALE/2, -300_000_000_000, 400_000_000_000), - (100*SCALE, 200_000_000_000, SCALE, -900_000_000_000, 800_000_000_000), - (100*SCALE, 200_000_000_000, 0, 0, 100_000_000_000), + ( + 100 * SCALE, + 200_000_000_000, + SCALE / 2, + -300_000_000_000, + 400_000_000_000, + ), + ( + 100 * SCALE, + 200_000_000_000, + SCALE, + -900_000_000_000, + 800_000_000_000, + ), + (100 * SCALE, 200_000_000_000, 0, 0, 100_000_000_000), ]; for &(f, alpha, beta, rho, nu) in params { for k_pct in (50..=200).step_by(10) { let k = f / 100 * k_pct as u128; - for &t in &[100_000_000_000u128, SCALE, 5*SCALE] { + for &t in &[100_000_000_000u128, SCALE, 5 * SCALE] { if let Ok(vol) = sabr_implied_vol(f, k, t, alpha, beta, rho, nu) { - assert!(vol > 0, "Non-positive vol: f={} k={} t={} vol={}", f, k, t, vol); + assert!( + vol > 0, + "Non-positive vol: f={} k={} t={} vol={}", + f, + k, + t, + vol + ); } } } @@ -902,14 +2520,29 @@ mod tests { let f = 100 * SCALE; let t = SCALE; let cases: &[(u128, u128, i128, u128)] = &[ - (200_000_000_000, SCALE/2, -500_000_000_000, 200_000_000_000), - (200_000_000_000, 700_000_000_000, -700_000_000_000, 300_000_000_000), + ( + 200_000_000_000, + SCALE / 2, + -500_000_000_000, + 200_000_000_000, + ), + ( + 200_000_000_000, + 700_000_000_000, + -700_000_000_000, + 300_000_000_000, + ), ]; for &(alpha, beta, rho, nu) in cases { - let vol_90 = sabr_implied_vol(f, 90*SCALE, t, alpha, beta, rho, nu).unwrap(); - let vol_110 = sabr_implied_vol(f, 110*SCALE, t, alpha, beta, rho, nu).unwrap(); - assert!(vol_90 > vol_110, - "Skew wrong: rho={} vol_90={} vol_110={}", rho, vol_90, vol_110); + let vol_90 = sabr_implied_vol(f, 90 * SCALE, t, alpha, beta, rho, nu).unwrap(); + let vol_110 = sabr_implied_vol(f, 110 * SCALE, t, alpha, beta, rho, nu).unwrap(); + assert!( + vol_90 > vol_110, + "Skew wrong: rho={} vol_90={} vol_110={}", + rho, + vol_90, + vol_110 + ); } } @@ -921,17 +2554,25 @@ mod tests { let rho = -300_000_000_000i128; let nu = 400_000_000_000u128; - for &k in &[90*SCALE, 95*SCALE, 100*SCALE, 105*SCALE, 110*SCALE] { + for &k in &[ + 90 * SCALE, + 95 * SCALE, + 100 * SCALE, + 105 * SCALE, + 110 * SCALE, + ] { let vol_exact = sabr_implied_vol(f, k, t, alpha, SCALE, rho, nu).unwrap(); let vol_near = sabr_implied_vol(f, k, t, alpha, 999_000_000_000, rho, nu).unwrap(); let diff = (vol_exact as i128 - vol_near as i128).abs(); let tol = vol_exact as i128 / 100; - assert!(diff < tol, + assert!( + diff < tol, "β continuity: k={} vol_1.0={} vol_0.999={} diff={}", - k/SCALE, vol_exact, vol_near, diff); + k / SCALE, + vol_exact, + vol_near, + diff + ); } } } - -#[cfg(test)] -include!("../test_data/sabr_reference_tests.rs"); diff --git a/src/transcendental.rs b/src/transcendental.rs index a486ec9..c5af39c 100644 --- a/src/transcendental.rs +++ b/src/transcendental.rs @@ -1,25 +1,144 @@ +use crate::arithmetic::{fp_div_i, fp_mul, fp_mul_i, fp_sqrt}; use crate::constants::*; use crate::error::SolMathError; -use crate::arithmetic::{fp_mul, fp_mul_i, fp_mul_i_round, fp_div_i, fp_sqrt}; -use crate::overflow::checked_mul_div_u; +use crate::exp_coeffs::{ + EXP2_PHASE_Q62, EXP_LN2_RESIDUAL_Q96, EXP_PHASES, EXP_PHASE_BITS, EXP_POLY_GUARD, + EXP_RAW_TO_Q63_FRAC_Q28, EXP_RAW_TO_Q63_HI, EXP_REMEZ_Q22, EXP_STEP_Q63, +}; +use crate::expm1_lut::{ + EXPM1_INV_LN2_Q56, EXPM1_LUT_SEGMENTS, EXPM1_LUT_STEP, EXPM1_LUT_STEP_SHIFT, + EXPM1_MID_EXP_RAW_Q22, EXPM1_RAW_TO_Q43_G31, EXPM1_R_MIN, +}; use crate::hp::pow_fixed_hp; +use crate::ln2_lut::{K_LN2_MAX, K_LN2_MIN, K_LN2_RAW}; +use crate::ln_lut::{ + LN_LUT_HALF_STEP, LN_LUT_MID_LOG, LN_LUT_SEGMENTS, LN_LUT_STEP, LN_Q42_RECIP_G32, +}; +use crate::overflow::checked_mul_div_u; + +#[inline(always)] +fn round_shift_signed(value: i128, shift: u32) -> i128 { + let half = 1i128 << (shift - 1); + if value >= 0 { + (value + half) >> shift + } else { + -((-value + half) >> shift) + } +} + +#[inline(always)] +fn round_shift_i64(value: i64, shift: u32) -> i64 { + let half = 1i64 << (shift - 1); + if value >= 0 { + (value + half) >> shift + } else { + -((-value + half) >> shift) + } +} + +#[inline(always)] +fn mul_q42(a: i64, b: i64) -> i64 { + round_shift_i64(a * b, 42) +} + +#[inline(always)] +fn mul_q43(a: i64, b: i64) -> i64 { + round_shift_i64(a * b, 43) +} + +#[inline(always)] +fn mul_q63_i64(a: i64, b: i64) -> i64 { + round_shift_signed(a as i128 * b as i128, 63) as i64 +} + +/// Wide-division-free local log kernel for a mantissa in `[SCALE, 2*SCALE)`. +#[inline] +fn ln_mantissa_lut(m: u128, k: i32) -> i128 { + debug_assert!(m >= SCALE && m < 2 * SCALE); + debug_assert!((K_LN2_MIN..=K_LN2_MAX).contains(&k)); + + // Preserve exact rounded logarithms for powers of two without entering + // the local polynomial around the first midpoint. + if m == SCALE { + return K_LN2_RAW[(k - K_LN2_MIN) as usize] as i128; + } + + let offset = m - SCALE; + // `offset < 1e12` and the step is below 1e9, so this exact index needs + // only a 64-bit division. Keeping it as u128 links the much costlier SBF + // wide-division path even though neither operand requires it. + let j = ((offset as u64) / (LN_LUT_STEP as u64)) as usize; + debug_assert!(j < LN_LUT_SEGMENTS); + let midpoint = SCALE + j as u128 * LN_LUT_STEP + LN_LUT_HALF_STEP; + let d = m as i64 - midpoint as i64; + + // q=(m-midpoint)/midpoint at Q42. The reciprocal carries 32 guard bits, + // and this conversion plus every polynomial product fits i64. + let q = round_shift_i64(d * LN_Q42_RECIP_G32[j], 32); + let q2 = mul_q42(q, q); + let q3 = mul_q42(q2, q); + let local_q42 = q - q2 / 2 + q3 / 3; + // One final wide conversion preserves a single rounding point. This is + // the only i128 multiplication in the local kernel. + let local_raw = round_shift_signed(local_q42 as i128 * SCALE_I, 42); + + let k_log = K_LN2_RAW[(k - K_LN2_MIN) as usize]; + LN_LUT_MID_LOG[j] as i128 + local_raw + k_log as i128 +} + +#[cold] +#[inline(never)] +fn normalize_ln_fallback(value: u128) -> (u128, i32) { + // SCALE has bit length 40. A bit-length estimate gets the mantissa into + // [2^39, 2^40) in one shift; because SCALE lies inside that interval, at + // most one corrected shift is needed. Re-shifting the original value is + // important: adjusting an already truncated mantissa would lose one bit. + let bit_length = 128 - value.leading_zeros() as i32; + let mut k = bit_length - 40; + let shift = |exponent: i32| { + if exponent >= 0 { + value >> exponent as u32 + } else { + value << (-exponent) as u32 + } + }; + let mut m = shift(k); + if m < SCALE { + k -= 1; + m = shift(k); + } else if m >= 2 * SCALE { + k += 1; + m = shift(k); + } + (m, k) +} + +#[inline] +fn normalize_ln(value: u128) -> (u128, i32) { + if value >= SCALE && value < 2 * SCALE { + (value, 0) + } else if value >= SCALE / 2 && value < SCALE { + (value * 2, -1) + } else { + normalize_ln_fallback(value) + } +} /// Natural logarithm: ln(x / SCALE) * SCALE. /// -/// 16-entry split-constant lookup + degree-3 Remez polynomial on narrow subinterval. -/// Combined sub-ULP correction (table + LN2 residuals in one rounding). +/// Wide-division-free 1,024-segment Q42 midpoint kernel with a cubic residual. /// /// - **x**: unsigned fixed-point at `SCALE` (1e12). Must be > 0. /// - **Returns**: `i128` at `SCALE`. Negative for x < SCALE, zero for x == SCALE. /// - **Errors**: `DomainError` if `x == 0`. -/// - **Accuracy**: max 3 ULP, median 1 ULP, 44% exact. +/// - **Accuracy**: max 2 ULP over the retained production and adversarial corpora. /// /// # Example /// ``` /// use solmath::{ln_fixed_i, SCALE}; /// // ln(2.0) ≈ 0.693147... /// let result = ln_fixed_i(2 * SCALE)?; -/// assert!((result - 693_147_180_560i128).abs() <= 3); +/// assert!((result - 693_147_180_560i128).abs() <= 2); /// # Ok::<(), solmath::SolMathError>(()) /// ``` pub fn ln_fixed_i(x: u128) -> Result { @@ -27,103 +146,78 @@ pub fn ln_fixed_i(x: u128) -> Result { return Err(SolMathError::DomainError); } - let mut m = x; - let mut k: i32 = 0; + // For |x-1| < 1e-6, the quadratic remainder is below half a raw + // output unit. This preserves raw increments that would be lost while + // converting the first midpoint residual to Q42. + if x.abs_diff(SCALE) < 1_000_000 { + return Ok(x as i128 - SCALE_I); + } + + let (m, k) = normalize_ln(x); + Ok(ln_mantissa_lut(m, k)) +} - // Primary reduction: m in [SCALE, 2*SCALE) - while m < SCALE { - m = m.checked_mul(2).ok_or(SolMathError::Overflow)?; - k -= 1; +/// Natural logarithm of one plus a signed fixed-point value. +/// +/// Computes `ln(1 + x / SCALE) * SCALE`. This is the fixed-point counterpart +/// of `log1p`/`ln_1p` and preserves small increments because `1 + x` is formed +/// exactly in the integer representation before the compensated near-one +/// logarithm kernel runs. +/// +/// - **x**: signed fixed-point at [`SCALE`] (1e12). +/// - **Returns**: `ln(1 + x)` at `SCALE`. +/// - **Errors**: [`SolMathError::DomainError`] when `x <= -SCALE`. +/// - **Accuracy**: max 2 ULP, P99 1 ULP, median 0 over 110,000 retained vectors. +/// +/// # Example +/// ``` +/// use solmath::{ln_1p_fixed, SCALE_I}; +/// +/// // ln(1.05) ≈ 0.048790164169 +/// let result = ln_1p_fixed(SCALE_I / 20)?; +/// assert!((result - 48_790_164_169).abs() <= 2); +/// # Ok::<(), solmath::SolMathError>(()) +/// ``` +pub fn ln_1p_fixed(x: i128) -> Result { + if x <= -SCALE_I { + return Err(SolMathError::DomainError); } - while m >= 2 * SCALE { - m /= 2; - k += 1; + if x == SCALE_I { + return Ok(LN2_I); } - let k_i = k as i128; - let m_i = m as i128; + // For |x| < 1e-6, |ln(1+x) - x| = x²/2 + O(x³), which is strictly + // below half a raw SCALE unit. Returning x is therefore correctly rounded + // in this interval and, unlike forming the atanh quotient at SCALE, + // preserves one-raw-unit increments. + if x.unsigned_abs() < 1_000_000 { + return Ok(x); + } - // Near x = 1: direct computation to avoid cancellation. - let offset = m - SCALE; - if offset < LN_TABLE_HALF_STEP { - let t_num = m_i - SCALE_I; - let t_den = m_i + SCALE_I; - // t_num ∈ (-SCALE_I, SCALE_I); t_num * SCALE_I < 1e24 ≪ i128::MAX (1.7e38). - let t = (t_num * SCALE_I + t_den / 2) / t_den; - let u = fp_mul_i_round(t, t)?; - // Horner additions: each fp_mul_i_round result ∈ (-SCALE_I, SCALE_I); - // LN_REMEZ_W* < SCALE_I, so each partial sum ∈ (-2·SCALE_I, 2·SCALE_I). Fits i128. - let p = fp_mul_i_round(LN_REMEZ_W3, u)? + LN_REMEZ_W2; - let p = fp_mul_i_round(p, u)? + LN_REMEZ_W1; - let p = fp_mul_i_round(p, u)? + LN_REMEZ_W0; - // 2 * t: t ∈ (-SCALE_I, SCALE_I), so 2*t ∈ (-2e12, 2e12). Fits i128. - let series_result = fp_mul_i_round(2 * t, p)?; - // Direct path: only LN2 correction (no table residual). - // |k_i| ≤ 127, LN2_LO ≈ 5.5e10; product < 7e12 ≪ i128::MAX. - let ln2_raw = k_i * LN2_LO; - let ln2_correction = if ln2_raw >= 0 { - (ln2_raw + SCALE_I / 2) / SCALE_I - } else { - (ln2_raw - SCALE_I / 2) / SCALE_I - }; - // series_result ≤ SCALE_I, k_i * LN2_I ≤ 127 * 6.9e11 ≈ 8.8e13, ln2_correction < 1; - // total sum ≪ i128::MAX. - return Ok(series_result + k_i * LN2_I + ln2_correction); - } - - // Table lookup. - let j = (offset / LN_TABLE_STEP) as usize; - let j = j.min(15); - - let m_j = SCALE + (2 * j as u128 + 1) * LN_TABLE_HALF_STEP; - let ln_m_j = LN_TABLE_16[j]; - let ln_m_j_lo = LN_TABLE_LO_16[j]; - - let m_j_i = m_j as i128; - let t_num = m_i - m_j_i; - let t_den = m_i + m_j_i; - // t_num ∈ (-SCALE_I/16, SCALE_I/16) after table reduction; t_num * SCALE_I < 6.25e10 * 1e12 ≪ i128::MAX. - let p_val = t_num * SCALE_I; - let t = (p_val + t_den / 2) / t_den; - // t's sub-ULP residual via multiply-subtract (no second division). - // t_rem = p_val - t * t_den: t ≤ SCALE_I/16, t_den ≤ 4*SCALE_I; product ≤ 2.5e23 ≪ i128::MAX. - let t_rem = p_val - t * t_den; // exact remainder - // t_rem < t_den ≤ 4*SCALE_I, so t_rem * SCALE_I ≤ 4e24 ≪ i128::MAX. - let t_lo = t_rem * SCALE_I / t_den; // scaled to sub-ULP units - - let u = fp_mul_i_round(t, t)?; - - // Horner additions: same bounds as direct path — each partial sum ∈ (-2·SCALE_I, 2·SCALE_I). - let p = fp_mul_i_round(LN_REMEZ_W3, u)? + LN_REMEZ_W2; - let p = fp_mul_i_round(p, u)? + LN_REMEZ_W1; - let p = fp_mul_i_round(p, u)? + LN_REMEZ_W0; - - // 2 * t: t ∈ (-SCALE_I/16, SCALE_I/16) here; 2*t < 1.25e11. Fits i128. - let series_result = fp_mul_i_round(2 * t, p)?; - - // COMBINED sub-ULP correction: table residual + LN2 residual + t residual. - // |k_i| ≤ 127, LN2_LO ≈ 5.5e10; k_i * LN2_LO < 7e12. Each of the three terms < 1e12; - // combined sum < 3e12 ≪ i128::MAX. - let combined_lo = ln_m_j_lo + k_i * LN2_LO + t_lo; - let correction = if combined_lo >= 0 { - (combined_lo + SCALE_I / 2) / SCALE_I + let one_plus_x = if x < 0 { + (SCALE_I + x) as u128 } else { - (combined_lo - SCALE_I / 2) / SCALE_I + SCALE.checked_add(x as u128).ok_or(SolMathError::Overflow)? }; - // series_result ≤ SCALE_I, ln_m_j ≤ LN_TABLE_16 max ≈ 0.7*SCALE_I, k_i * LN2_I ≤ 8.8e13, - // correction ≈ 0 to 1; total sum ≪ i128::MAX. - Ok(series_result + ln_m_j + k_i * LN2_I + correction) + // Normalize once, then use a dedicated 1,024-segment Q42 kernel. Common + // rate inputs stay in the first two branches and execute no loop. + let (m, k) = normalize_ln(one_plus_x); + + Ok(ln_mantissa_lut(m, k)) } /// Exponential: e^(x / SCALE) * SCALE. /// -/// Remez rational approximation with LN2 residual correction. +/// Division-free degree-5 near-minimax approximation after ln(2)/32 reduction. /// /// - **x**: signed fixed-point at `SCALE` (1e12). -/// - **Returns**: `i128` at `SCALE`. Always positive for valid inputs. +/// - **Returns**: non-negative `i128` at `SCALE`; very negative valid inputs +/// can round to zero. /// - **Errors**: `Overflow` if `x >= 40 * SCALE`. Returns `Ok(0)` for `x <= -40 * SCALE`. -/// - **Accuracy**: max 1 ULP. +/// - **Accuracy**: the reduced kernel has a conservative relative bound below +/// `4.833e-15`. Absolute raw error grows with the reconstructed power of two; +/// see the retained production/adversarial measurements in `VALIDATION.md`. /// /// # Example /// ``` @@ -136,58 +230,78 @@ pub fn ln_fixed_i(x: u128) -> Result { pub fn exp_fixed_i(x: i128) -> Result { let max_x = 40 * SCALE_I; - if x <= -max_x { return Ok(0); } - if x >= max_x { return Err(SolMathError::Overflow); } - if x == 0 { return Ok(SCALE_I); } - - // Range reduction with split LN2 correction. - // LN2_I overshoots true ln(2)×SCALE (LN2_LO < 0), so k*LN2_I is too large - // and r = x - k*LN2_I is too small. Subtract the negative correction to add back. - let mut k = x / LN2_I; - let ln2_correction = { - // |k| ≤ 57 (x < 40*SCALE_I, LN2_I ≈ 6.9e11), LN2_LO ≈ 5.5e10; product < 3.1e12 ≪ i128::MAX. - let raw = k * LN2_LO; - if raw >= 0 { (raw + SCALE_I / 2) / SCALE_I } else { (raw - SCALE_I / 2) / SCALE_I } - }; - // x < 40*SCALE_I ≈ 4e13; k * LN2_I ≤ 57 * 6.9e11 ≈ 3.9e13; r ∈ [-LN2/2, LN2/2] ≈ ±3.5e11. - let mut r = x - k * LN2_I - ln2_correction; - - let half_ln2 = LN2_I / 2; - if r > half_ln2 { - k += 1; - // r ∈ (LN2/2, LN2_I]; r - LN2_I ∈ (-LN2/2, 0]. Stays in [-LN2/2, LN2/2]. - r -= LN2_I; - } else if r < -half_ln2 { - k -= 1; - // r ∈ [-LN2_I, -LN2/2); r + LN2_I ∈ (0, LN2/2]. Stays in [-LN2/2, LN2/2]. - r += LN2_I; + if x <= -max_x { + return Ok(0); + } + if x >= max_x { + return Err(SolMathError::Overflow); + } + // For |x| < 1e-6, the exp Taylor remainder is strictly below half a + // raw unit. This is correctly rounded and preserves tiny rate inputs. + if (-1_000_000..1_000_000).contains(&x) { + return Ok(SCALE_I + x); } - // Remez rational formula - let xx = fp_mul_i_round(r, r)?; - - // Horner: poly = P1 + xx*(P2 + xx*(P3 + xx*(P4 + xx*P5))) - // EXP_REMEZ_P* < SCALE_I; each partial sum ∈ (-2·SCALE_I, 2·SCALE_I). Fits i128. - let poly = fp_mul_i_round(xx, EXP_REMEZ_P5)? + EXP_REMEZ_P4; - let poly = fp_mul_i_round(xx, poly)? + EXP_REMEZ_P3; - let poly = fp_mul_i_round(xx, poly)? + EXP_REMEZ_P2; - let poly = fp_mul_i_round(xx, poly)? + EXP_REMEZ_P1; - - // c = r - poly * r^2 - // r ∈ [-LN2/2, LN2/2] ≈ ±3.5e11; fp_mul_i_round(poly, xx) ≤ SCALE_I; c ∈ [-2·SCALE_I, 2·SCALE_I]. - let c = r - fp_mul_i_round(poly, xx)?; - - // exp(r) = 1 + r + r*c/(2-c) - // SCALE_I + r: r ≤ LN2/2 ≈ 3.5e11, SCALE_I = 1e12; sum < 1.35e12. - // 2 * SCALE_I - c: c ≤ 2·SCALE_I; denominator ∈ (0, 4·SCALE_I). Cannot underflow (c < 2*SCALE_I for valid r). - let rc = fp_mul_i_round(r, c)?; - let sum = SCALE_I + r + fp_div_i(rc, 2 * SCALE_I - c)?; + // First reduce to the nearest full ln(2) octave using only i64. A split + // reciprocal converts the small raw residual to Q63 without a wide + // range-reduction multiply, then restores LN2_I's sub-raw residual. + let x64 = x as i64; + let octave_estimate = round_shift_i64(x64 * EXPM1_INV_LN2_Q56, 56) as i32; + let raw_residual = x64 - octave_estimate as i64 * LN2_I as i64; + let scaled_residual = raw_residual * EXP_RAW_TO_Q63_HI + + round_shift_i64(raw_residual * EXP_RAW_TO_Q63_FRAC_Q28, 28); + let octave_residual_q63 = round_shift_i64(scaled_residual, 1) + - round_shift_i64(octave_estimate as i64 * EXP_LN2_RESIDUAL_Q96, 33); + + // Split the octave into 32 cells. The proposal uses raw i64 arithmetic; + // the Q63 check makes the final cell exact at every reduction seam. + let mut subcell = round_shift_i64( + raw_residual * EXPM1_INV_LN2_Q56, + (56 - EXP_PHASE_BITS) as u32, + ) as i32; + let mut r_q63 = octave_residual_q63 - subcell as i64 * EXP_STEP_Q63; + let half_step_q63 = (EXP_STEP_Q63 + 1) / 2; + if r_q63 > half_step_q63 { + subcell += 1; + r_q63 -= EXP_STEP_Q63; + } else if r_q63 < -half_step_q63 { + subcell -= 1; + r_q63 += EXP_STEP_Q63; + } + debug_assert!(r_q63.abs() <= half_step_q63 + 1); + + let poly = mul_q63_i64(EXP_REMEZ_Q22[0], r_q63) + EXP_REMEZ_Q22[1]; + let poly = mul_q63_i64(poly, r_q63) + EXP_REMEZ_Q22[2]; + let poly = mul_q63_i64(poly, r_q63) + EXP_REMEZ_Q22[3]; + let poly = mul_q63_i64(poly, r_q63) + EXP_REMEZ_Q22[4]; + let poly = mul_q63_i64(poly, r_q63) + EXP_REMEZ_Q22[5]; + + // cell = 32*octave + phase. These constants reconstruct only the + // fractional power of two; they are not a sampled answer table. + let cell = octave_estimate * EXP_PHASES as i32 + subcell; + let octave = cell >> EXP_PHASE_BITS; + let phase = (cell & (EXP_PHASES as i32 - 1)) as usize; + let (guarded, guard) = if phase == 0 { + (poly as i128, EXP_POLY_GUARD) + } else { + ( + poly as i128 * EXP2_PHASE_Q62[phase] as i128, + EXP_POLY_GUARD + 62, + ) + }; - // Multiply by 2^k - if k >= 0 { - sum.checked_shl(k as u32).ok_or(SolMathError::Overflow) + // Fold phase and octave reconstruction into one final rounding point. + let shift = guard - octave; + if shift >= 128 { + Ok(0) + } else if shift > 0 { + Ok(round_shift_signed(guarded, shift as u32)) + } else if shift == 0 { + Ok(guarded) } else { - Ok(sum >> (-k) as u32) + guarded + .checked_shl((-shift) as u32) + .ok_or(SolMathError::Overflow) } } @@ -245,16 +359,62 @@ pub fn pow_fixed(base: u128, exponent: u128) -> Result { } } + // The standard log now preserves raw near-one deltas, but its sub-ULP + // log error can still be amplified by a large real exponent. Keep that + // narrow composition on the HP path; ordinary exponents and the special + // square/square-root branches above retain the cheaper implementation. + if base.abs_diff(SCALE) < 1_000_000 && exponent > SCALE { + return pow_fixed_hp(base, exponent); + } + // General case: exp(exponent * ln(base)) let ln_base = ln_fixed_i(base)?; // i128 - let exp_i = exponent as i128; - let product = fp_mul_i(exp_i, ln_base)?; // exponent * ln(base) + if ln_base == 0 && base != SCALE { + // Standard-scale ln cannot resolve bases one or a few raw units from + // one. A huge exponent would otherwise turn that lost bit into a + // catastrophic result of exactly one. + return pow_fixed_hp(base, exponent); + } + let exp_i = match i128::try_from(exponent) { + Ok(v) => v, + Err(_) if base < SCALE => return Ok(0), + Err(_) => return Err(SolMathError::Overflow), + }; + let product = match fp_mul_i(exp_i, ln_base) { + Ok(v) => v, + Err(SolMathError::Overflow) if base < SCALE => return Ok(0), + Err(e) => return Err(e), + }; // exponent * ln(base) let result = exp_fixed_i(product)?; - Ok(if result <= 0 { - 0 - } else { - result as u128 - }) + Ok(if result <= 0 { 0 } else { result as u128 }) +} + +#[cfg(test)] +mod adversarial_power_tests { + use super::*; + + #[test] + fn huge_positive_exponents_preserve_direction() { + assert_eq!(pow_fixed(SCALE / 2, 1u128 << 127), Ok(0)); + assert_eq!( + pow_fixed(2 * SCALE, 1u128 << 127), + Err(SolMathError::Overflow) + ); + assert_eq!(pow_fixed(SCALE / 10, i128::MAX as u128), Ok(0)); + } + + #[test] + fn near_one_large_exponent_uses_high_precision_log() { + let exponent = 1u128 << 80; + assert_eq!( + pow_fixed(SCALE + 1, exponent), + pow_fixed_hp(SCALE + 1, exponent) + ); + assert_eq!( + pow_fixed(SCALE + 1, 1u128 << 86), + Err(SolMathError::Overflow) + ); + } } /// Integer power: base^n via repeated squaring or HP path. @@ -269,26 +429,43 @@ pub fn pow_int(base: u128, n: u128) -> Result { match n { 0 => Ok(SCALE), 1 => Ok(base), + _ if base == 0 => Ok(0), + _ if base == SCALE => Ok(SCALE), 2 => fp_mul(base, base), 3 => Ok(fp_mul(fp_mul(base, base)?, base)?), - 4 => { let x2 = fp_mul(base, base)?; fp_mul(x2, x2) }, + 4 => { + let x2 = fp_mul(base, base)?; + fp_mul(x2, x2) + } _ => { // Check if n * ln(base) fits in HP exp's working range let ln_base = ln_fixed_i(base)?; - let total = match (n as i128).checked_mul(ln_base) { + let n_i = match i128::try_from(n) { + Ok(value) => value, + Err(_) if base < SCALE => return Ok(0), + Err(_) => return Err(SolMathError::Overflow), + }; + let total = match n_i.checked_mul(ln_base) { Some(v) => v, - None => if ln_base > 0 { return Err(SolMathError::Overflow) } else { return Ok(0) }, + None => { + if ln_base > 0 { + return Err(SolMathError::Overflow); + } else { + return Ok(0); + } + } }; - if total.abs() < 39 * SCALE_I { - pow_fixed_hp(base, n * SCALE) + if total.unsigned_abs() < (39 * SCALE_I) as u128 { + let exponent = n.checked_mul(SCALE).ok_or(SolMathError::Overflow)?; + pow_fixed_hp(base, exponent) } else { // Split: base^n = (base^(n/2))² × base^(n%2) let half = pow_int(base, n / 2)?; - let mut result = checked_mul_div_u(half, half, SCALE) - .ok_or(SolMathError::Overflow)?; + let mut result = + checked_mul_div_u(half, half, SCALE).ok_or(SolMathError::Overflow)?; if n % 2 == 1 { - result = checked_mul_div_u(result, base, SCALE) - .ok_or(SolMathError::Overflow)?; + result = + checked_mul_div_u(result, base, SCALE).ok_or(SolMathError::Overflow)?; } Ok(result) } @@ -326,7 +503,8 @@ pub fn pow_fixed_i(base: i128, exponent: i128) -> Result { // Negative exponent: 1 / pow(base, |exponent|) if exponent < 0 { - let pos_result = pow_fixed_i(base, -exponent)?; + let positive_exponent = exponent.checked_neg().ok_or(SolMathError::Overflow)?; + let pos_result = pow_fixed_i(base, positive_exponent)?; if pos_result == 0 { return Err(SolMathError::Overflow); // 1/0 → overflow } @@ -361,44 +539,172 @@ pub fn pow_fixed_i(base: i128, exponent: i128) -> Result { /// exp(x) - 1 with better precision near zero: (e^(x/SCALE) - 1) * SCALE. /// -/// Uses degree-11 Taylor on [-0.5, 0.5] to avoid catastrophic cancellation, -/// falls back to `exp_fixed_i(x) - SCALE` outside that range. +/// Uses a 1,292-segment midpoint table and Q43 cubic residual with one wide +/// multiply. A correctly-rounded direct path preserves raw increments near zero. /// /// - **x**: signed fixed-point at `SCALE` (1e12). /// - **Returns**: `i128` at `SCALE`. Near zero for small x. -/// - **Errors**: `Overflow` if `exp_fixed_i` overflows (|x| > 0.5 and x >= 40*SCALE). -/// - **Accuracy**: max 3 ULP. +/// - **Errors**: `Overflow` when `x >= 40*SCALE`; saturates to `-SCALE` when +/// `x <= -40*SCALE`. +/// - **Accuracy**: for `|x| <= 2`, max 3 ULP, P99 2 ULP, median 0 over +/// 60,000 retained production vectors. Measured full-domain relative error +/// is below one part per trillion for outputs with magnitude at least one. pub fn expm1_fixed(x: i128) -> Result { - let half = SCALE_I / 2; - if x > half || x < -half { - // exp_fixed_i(x) ∈ (0, e^40 * SCALE_I); subtracting SCALE_I is safe — no overflow. - return Ok(exp_fixed_i(x)? - SCALE_I); - } - if x == 0 { return Ok(0); } - - const C11: i128 = 25_052; - const C10: i128 = 275_573; - const C9: i128 = 2_755_732; - const C8: i128 = 24_801_587; - const C7: i128 = 198_412_698; - const C6: i128 = 1_388_888_889; - const C5: i128 = 8_333_333_333; - const C4: i128 = 41_666_666_667; - const C3: i128 = 166_666_666_667; - const C2: i128 = 500_000_000_000; - const C1: i128 = SCALE_I; - - // x ∈ [-0.5·SCALE_I, 0.5·SCALE_I]. Horner accumulation: fp_mul_i_round result ≤ SCALE_I/2; - // each Cn ≤ SCALE_I, so every partial sum ∈ (-2·SCALE_I, 2·SCALE_I). Fits i128. - let p = fp_mul_i_round(x, C11)? + C10; - let p = fp_mul_i_round(x, p)? + C9; - let p = fp_mul_i_round(x, p)? + C8; - let p = fp_mul_i_round(x, p)? + C7; - let p = fp_mul_i_round(x, p)? + C6; - let p = fp_mul_i_round(x, p)? + C5; - let p = fp_mul_i_round(x, p)? + C4; - let p = fp_mul_i_round(x, p)? + C3; - let p = fp_mul_i_round(x, p)? + C2; - let p = fp_mul_i_round(x, p)? + C1; - Ok(fp_mul_i_round(x, p)?) + let limit = 40 * SCALE_I; + if x <= -limit { + return Ok(-SCALE_I); + } + if x >= limit { + return Err(SolMathError::Overflow); + } + + // On |x| < 1e-6, expm1(x)-x = x²/2+O(x³) is below half a raw unit. + if x.unsigned_abs() < 1_000_000 { + return Ok(x); + } + + let x64 = x as i64; // |x| < 40*SCALE_I < i64::MAX. + let mut k = round_shift_i64(x64 * EXPM1_INV_LN2_Q56, 56) as i32; + debug_assert!((K_LN2_MIN..=64).contains(&k)); + let mut r = x64 - K_LN2_RAW[(k - K_LN2_MIN) as usize]; + const HALF_LN2_RAW: i64 = 346_573_590_280; + if r > HALF_LN2_RAW { + k += 1; + r = x64 - K_LN2_RAW[(k - K_LN2_MIN) as usize]; + } else if r < -HALF_LN2_RAW { + k -= 1; + r = x64 - K_LN2_RAW[(k - K_LN2_MIN) as usize]; + } + + let offset = (r - EXPM1_R_MIN) as u64; + debug_assert!(r >= EXPM1_R_MIN); + let j = ((offset >> EXPM1_LUT_STEP_SHIFT) as usize).min(EXPM1_LUT_SEGMENTS - 1); + let midpoint = EXPM1_R_MIN + j as i64 * EXPM1_LUT_STEP + EXPM1_LUT_STEP / 2; + let delta = r - midpoint; + + // exp(delta) at Q43. |delta| <= 2^28 raw, so all products fit i64; + // the omitted quartic contributes less than 0.001 raw unit before scaling. + let q = round_shift_i64(delta * EXPM1_RAW_TO_Q43_G31, 31); + let q2 = mul_q43(q, q); + let q3 = mul_q43(q2, q); + let local_q43 = (1i64 << 43) + q + q2 / 2 + q3 / 6; + + // The midpoint already contains decimal SCALE with 22 binary guard bits, + // so one wide product produces the final-scale value without a second + // wide multiplication by SCALE. + let exp_r_raw_q22 = + round_shift_signed(EXPM1_MID_EXP_RAW_Q22[j] as i128 * local_q43 as i128, 43); + let shift = 22 - k; + let exp_x = if shift > 0 { + round_shift_signed(exp_r_raw_q22, shift as u32) + } else if shift == 0 { + exp_r_raw_q22 + } else { + exp_r_raw_q22 + .checked_shl((-shift) as u32) + .ok_or(SolMathError::Overflow)? + }; + Ok(exp_x - SCALE_I) +} + +#[cfg(test)] +mod boundary_tests { + use super::*; + + #[test] + fn pow_fixed_rejects_unsigned_exponent_that_cannot_be_signed() { + assert_eq!( + pow_fixed(2 * SCALE, i128::MAX as u128 + 1), + Err(SolMathError::Overflow) + ); + } + + #[test] + fn pow_int_handles_zero_and_extreme_exponents_consistently() { + assert_eq!(pow_int(0, 5), Ok(0)); + assert_eq!(pow_int(SCALE, u128::MAX), Ok(SCALE)); + assert_eq!(pow_int(SCALE / 2, i128::MAX as u128 + 1), Ok(0)); + assert_eq!( + pow_int(2 * SCALE, i128::MAX as u128 + 1), + Err(SolMathError::Overflow) + ); + } + + #[test] + fn pow_fixed_i_rejects_unnegatable_min_exponent() { + assert_eq!( + pow_fixed_i(2 * SCALE_I, i128::MIN), + Err(SolMathError::Overflow) + ); + } + + #[test] + fn ln_1p_has_explicit_domain_and_exact_special_values() { + assert_eq!(ln_1p_fixed(-SCALE_I), Err(SolMathError::DomainError)); + assert_eq!(ln_1p_fixed(i128::MIN), Err(SolMathError::DomainError)); + assert_eq!(ln_1p_fixed(0), Ok(0)); + assert_eq!(ln_1p_fixed(SCALE_I), ln_fixed_i(2 * SCALE)); + assert!(ln_1p_fixed(i128::MAX).is_ok()); + } + + #[test] + fn ln_1p_preserves_raw_increments_near_zero() { + assert_eq!(ln_1p_fixed(1), Ok(1)); + assert_eq!(ln_1p_fixed(-1), Ok(-1)); + assert_eq!(ln_1p_fixed(2), Ok(2)); + assert_eq!(ln_1p_fixed(-2), Ok(-2)); + } + + #[test] + fn ln_1p_and_expm1_round_trip_financial_rates() { + for x in [ + -900_000_000_000, + -500_000_000_000, + -10_000_000_000, + -1_000_000, + 1_000_000, + 10_000_000_000, + 500_000_000_000, + 5 * SCALE_I, + ] { + let recovered = expm1_fixed(ln_1p_fixed(x).unwrap()).unwrap(); + assert!((recovered - x).abs() <= 12, "x={x}, recovered={recovered}"); + } + } + + #[test] + fn expm1_has_explicit_limits_and_preserves_raw_increments() { + let limit = 40 * SCALE_I; + assert_eq!(expm1_fixed(i128::MIN), Ok(-SCALE_I)); + assert_eq!(expm1_fixed(-limit), Ok(-SCALE_I)); + assert_eq!(expm1_fixed(limit), Err(SolMathError::Overflow)); + assert_eq!(expm1_fixed(i128::MAX), Err(SolMathError::Overflow)); + assert_eq!(expm1_fixed(-1), Ok(-1)); + assert_eq!(expm1_fixed(0), Ok(0)); + assert_eq!(expm1_fixed(1), Ok(1)); + } + + #[test] + fn expm1_matches_known_ordinary_values() { + assert!(expm1_fixed(SCALE_I).unwrap().abs_diff(1_718_281_828_459) <= 3); + assert!(expm1_fixed(-SCALE_I).unwrap().abs_diff(-632_120_558_829) <= 1); + } + + #[test] + fn expm1_is_monotone_across_every_lut_boundary_and_exponent() { + for k in -58..=58 { + let k_ln2 = K_LN2_RAW[(k - K_LN2_MIN) as usize] as i128; + for j in 1..EXPM1_LUT_SEGMENTS { + let boundary = EXPM1_R_MIN as i128 + j as i128 * EXPM1_LUT_STEP as i128; + let x_left = k_ln2 + boundary - 1; + let x_right = k_ln2 + boundary; + if x_left <= -40 * SCALE_I || x_right >= 40 * SCALE_I { + continue; + } + let left = expm1_fixed(x_left).unwrap(); + let right = expm1_fixed(x_right).unwrap(); + assert!(left <= right, "k={k}, j={j}: {left} > {right}"); + } + } + } } diff --git a/src/trig.rs b/src/trig.rs index c49d2df..2caadf7 100644 --- a/src/trig.rs +++ b/src/trig.rs @@ -1,7 +1,20 @@ -use crate::constants::*; use crate::arithmetic::fp_mul_i_fast; +use crate::constants::*; use crate::error::SolMathError; +/// Largest angle covered by the two-word period reduction error bound: +/// 1e25 raw = 1e13 radians at SCALE. +const MAX_TRIG_ANGLE: i128 = 10_000_000_000_000_000_000_000_000; + +#[inline] +fn validate_angle(x: i128) -> Result<(), SolMathError> { + if x.unsigned_abs() > MAX_TRIG_ANGLE as u128 { + Err(SolMathError::DomainError) + } else { + Ok(()) + } +} + /// Core sin on [−π/4, π/4]: sin(x) = x × P(x²). Internal — called by sin_fixed. pub(crate) fn sin_core(x: i128) -> Result { let t = fp_mul_i_fast(x, x); @@ -30,9 +43,30 @@ pub(crate) fn cos_core(x: i128) -> Result { /// Reduce angle to (−π, π] with Cody-Waite compensation. Internal. pub(crate) fn reduce_mod_2pi(x: i128) -> i128 { - // Use Euclidean reduction against the full fixed-point period so every i128 input, - // including i128::MIN, lands in a small bounded range before polynomial evaluation. + // Two-stage reduction: Euclidean reduction against the rounded period + // (handles every i128 input, including i128::MIN), then fold back q times + // the sub-ULP residual — TWO_PI_SCALE overshoots 2π by ~0.41 ULP, so the + // plain remainder drifts ~0.41 ULP per elapsed period (61K ULP of phase + // error at |x| ≈ 1e6 rad without this). Each pass shrinks |q| by ~13 + // orders of magnitude, so at most 3 passes run even from i128::MAX. let mut r = x.rem_euclid(TWO_PI_SCALE); + let mut q = x.div_euclid(TWO_PI_SCALE); + while q != 0 { + // |q| ≤ i128::MAX/TWO_PI_SCALE ≈ 2.7e25; |q·TWO_PI_LO| ≤ 1.2e37 ≪ i128::MAX. + let corr = q * (-TWO_PI_LO); // sub-ULP units + let corr_ulp = if corr >= 0 { + (corr + SCALE_I / 2) / SCALE_I + } else { + (corr - SCALE_I / 2) / SCALE_I + }; + if corr_ulp == 0 { + break; + } + // r < TWO_PI_SCALE ≈ 6.3e12, |corr_ulp| ≤ 1.2e25: sum fits i128. + let t = r + corr_ulp; + r = t.rem_euclid(TWO_PI_SCALE); + q = t.div_euclid(TWO_PI_SCALE); + } if r > PI_SCALE { r -= TWO_PI_SCALE; } @@ -41,9 +75,10 @@ pub(crate) fn reduce_mod_2pi(x: i128) -> i128 { /// Sine of angle x in radians at SCALE. /// -/// - **x**: signed fixed-point radians at `SCALE` (1e12). Any value accepted. +/// - **x**: signed fixed-point radians at `SCALE` (1e12), with +/// `|x| <= 1e25` raw (1e13 radians). /// - **Returns**: `Result` — value at `SCALE`, in [-SCALE, SCALE]. -/// - **Accuracy**: max 2 ULP, ~47% exact. +/// - **Accuracy**: max ~3 ULP over the supported range. /// /// # Example /// ``` @@ -54,6 +89,7 @@ pub(crate) fn reduce_mod_2pi(x: i128) -> i128 { /// assert!((result - SCALE_I).abs() <= 2); /// ``` pub fn sin_fixed(x: i128) -> Result { + validate_angle(x)?; let mut xx = reduce_mod_2pi(x); let sign = if xx < 0 { xx = -xx; @@ -77,10 +113,12 @@ pub fn sin_fixed(x: i128) -> Result { /// Cosine of angle x in radians at SCALE. /// -/// - **x**: signed fixed-point radians at `SCALE` (1e12). Any value accepted. +/// - **x**: signed fixed-point radians at `SCALE` (1e12), with +/// `|x| <= 1e25` raw (1e13 radians). /// - **Returns**: `Result` — value at `SCALE`, in [-SCALE, SCALE]. -/// - **Accuracy**: max 2 ULP, ~47% exact. +/// - **Accuracy**: max ~3 ULP over the supported range. pub fn cos_fixed(x: i128) -> Result { + validate_angle(x)?; let mut xx = reduce_mod_2pi(x); xx = if xx < 0 { -xx } else { xx }; let cos_sign = if xx > PI_OVER_2_SCALE { @@ -102,10 +140,12 @@ pub fn cos_fixed(x: i128) -> Result { /// Fused sine and cosine: returns `(sin(x), cos(x))` sharing one angle reduction. /// -/// - **x**: signed fixed-point radians at `SCALE` (1e12). Any value accepted. +/// - **x**: signed fixed-point radians at `SCALE` (1e12), with +/// `|x| <= 1e25` raw (1e13 radians). /// - **Returns**: `Result<(i128, i128), SolMathError>` — `(sin, cos)` each at `SCALE`. -/// - **Accuracy**: max 2 ULP each, ~47% exact. +/// - **Accuracy**: max ~3 ULP each over the supported range. pub fn sincos_fixed(x: i128) -> Result<(i128, i128), SolMathError> { + validate_angle(x)?; let mut xx = reduce_mod_2pi(x); let sin_sign = if xx < 0 { xx = -xx; diff --git a/test_data/heston_reference_tests.rs b/test_data/heston_reference_tests.rs deleted file mode 100644 index dc93bbc..0000000 --- a/test_data/heston_reference_tests.rs +++ /dev/null @@ -1,10008 +0,0 @@ -// Auto-generated from QuantLib. Do not edit. -// 500 of 100000 vectors (every 200th) - -#[cfg(test)] -mod quantlib_heston { - use crate::heston::heston_price; - - #[test] - fn ql_heston_0000() { - // S=100.0, K=80.0, T=0.1, r=0.0 - // v0=0.01, kappa=0.5, theta=0.01, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 500000000000u128, 10000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 20000000072912u128; - let exp_put = 72912u128; - let tol = 325000000000u128; // $0.33 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#0 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#0 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0001() { - // S=100.0, K=90.0, T=0.5, r=0.0 - // v0=0.01, kappa=0.5, theta=0.01, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 500000000000u128, 10000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 10217470847310u128; - let exp_put = 217470847310u128; - let tol = 175000000000u128; // $0.17 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#1 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#1 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0002() { - // S=100.0, K=105.0, T=2.0, r=0.0 - // v0=0.01, kappa=0.5, theta=0.01, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 500000000000u128, 10000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 2746636557217u128; - let exp_put = 7746636557217u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#2 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#2 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0003() { - // S=100.0, K=120.0, T=0.25, r=0.0 - // v0=0.01, kappa=0.5, theta=0.01, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 500000000000u128, 10000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 1646919993u128; - let exp_put = 20001646919993u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#3 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#3 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0004() { - // S=100.0, K=90.0, T=2.0, r=0.0 - // v0=0.01, kappa=0.5, theta=0.01, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 500000000000u128, 10000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 11547100966760u128; - let exp_put = 1547100966760u128; - let tol = 283333333333u128; // $0.28 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#4 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#4 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0005() { - // S=100.0, K=115.0, T=0.25, r=0.0 - // v0=0.01, kappa=0.5, theta=0.01, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 500000000000u128, 10000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 21988666967u128; - let exp_put = 15021988666967u128; - let tol = 589130434783u128; // $0.59 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#5 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#5 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0006() { - // S=100.0, K=115.0, T=2.0, r=0.0 - // v0=0.01, kappa=0.5, theta=0.04, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 500000000000u128, 40000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 2544193485949u128; - let exp_put = 17544193485949u128; - let tol = 89130434783u128; // $0.09 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#6 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#6 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0007() { - // S=100.0, K=90.0, T=0.5, r=0.0 - // v0=0.01, kappa=0.5, theta=0.04, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 500000000000u128, 40000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 10627224257469u128; - let exp_put = 627224257469u128; - let tol = 83333333333u128; // $0.08 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#7 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#7 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0008() { - // S=100.0, K=115.0, T=2.0, r=0.0 - // v0=0.01, kappa=0.5, theta=0.04, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 500000000000u128, 40000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 602174638364u128; - let exp_put = 15602174638364u128; - let tol = 289130434783u128; // $0.29 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#8 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#8 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0009() { - // S=100.0, K=90.0, T=0.5, r=0.0 - // v0=0.01, kappa=0.5, theta=0.04, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 500000000000u128, 40000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 10852386149813u128; - let exp_put = 852386149813u128; - let tol = 483333333333u128; // $0.48 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#9 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#9 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0010() { - // S=100.0, K=115.0, T=2.0, r=0.0 - // v0=0.01, kappa=0.5, theta=0.04, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 500000000000u128, 40000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 2595429472756u128; - let exp_put = 17595429472756u128; - let tol = 289130434783u128; // $0.29 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#10 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#10 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0011() { - // S=100.0, K=110.0, T=0.5, r=0.0 - // v0=0.01, kappa=0.5, theta=0.04, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 500000000000u128, 40000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 587757090564u128; - let exp_put = 10587757090564u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#11 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#11 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0012() { - // S=100.0, K=80.0, T=0.1, r=0.0 - // v0=0.01, kappa=0.5, theta=0.09, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 500000000000u128, 90000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 20000000000659u128; - let exp_put = 659u128; - let tol = 415000000000u128; // $0.41 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#12 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#12 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0013() { - // S=100.0, K=90.0, T=0.5, r=0.0 - // v0=0.01, kappa=0.5, theta=0.09, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 500000000000u128, 90000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 10784941945958u128; - let exp_put = 784941945958u128; - let tol = 83333333333u128; // $0.08 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#13 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#13 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0014() { - // S=100.0, K=100.0, T=2.0, r=0.0 - // v0=0.01, kappa=0.5, theta=0.09, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 500000000000u128, 90000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 10236940976666u128; - let exp_put = 10236940976666u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#14 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#14 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0015() { - // S=100.0, K=110.0, T=0.25, r=0.0 - // v0=0.01, kappa=0.5, theta=0.09, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 500000000000u128, 90000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 81987040677u128; - let exp_put = 10081987040677u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#15 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#15 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0016() { - // S=100.0, K=120.0, T=1.0, r=0.0 - // v0=0.01, kappa=0.5, theta=0.09, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 1000000000000u128, - 10000000000u128, 500000000000u128, 90000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 233690466201u128; - let exp_put = 20233690466201u128; - let tol = 600000000000u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#16 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#16 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0017() { - // S=100.0, K=110.0, T=0.25, r=0.0 - // v0=0.01, kappa=0.5, theta=0.16, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 500000000000u128, 160000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 194453665794u128; - let exp_put = 10194453665794u128; - let tol = 625000000000u128; // $0.62 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#17 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#17 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0018() { - // S=100.0, K=80.0, T=2.0, r=0.0 - // v0=0.01, kappa=0.5, theta=0.16, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 500000000000u128, 160000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 25673362533135u128; - let exp_put = 5673362533135u128; - let tol = 325000000000u128; // $0.33 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#18 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#18 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0019() { - // S=100.0, K=95.0, T=0.25, r=0.0 - // v0=0.01, kappa=0.5, theta=0.16, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 500000000000u128, 160000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 6089501970565u128; - let exp_put = 1089501970565u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#19 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#19 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0020() { - // S=100.0, K=110.0, T=1.0, r=0.0 - // v0=0.01, kappa=0.5, theta=0.16, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 1000000000000u128, - 10000000000u128, 500000000000u128, 160000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 4249652725013u128; - let exp_put = 14249652725013u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#20 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#20 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0021() { - // S=100.0, K=80.0, T=0.25, r=0.0 - // v0=0.01, kappa=0.5, theta=0.16, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 500000000000u128, 160000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 20017446619429u128; - let exp_put = 17446619429u128; - let tol = 325000000000u128; // $0.33 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#21 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#21 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0022() { - // S=100.0, K=90.0, T=1.0, r=0.0 - // v0=0.01, kappa=0.5, theta=0.16, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 1000000000000u128, - 10000000000u128, 500000000000u128, 160000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 13251120983244u128; - let exp_put = 3251120983244u128; - let tol = 583333333333u128; // $0.58 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#22 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#22 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0023() { - // S=100.0, K=115.0, T=0.1, r=0.0 - // v0=0.01, kappa=1.0, theta=0.01, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 1000000000000u128, 10000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 1427895u128; - let exp_put = 15000001427895u128; - let tol = 289130434783u128; // $0.29 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#23 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#23 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0024() { - // S=100.0, K=85.0, T=1.0, r=0.0 - // v0=0.01, kappa=1.0, theta=0.01, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 1000000000000u128, - 10000000000u128, 1000000000000u128, 10000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 15492129403718u128; - let exp_put = 492129403718u128; - let tol = 102941176471u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#24 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#24 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0025() { - // S=100.0, K=100.0, T=0.1, r=0.0 - // v0=0.01, kappa=1.0, theta=0.01, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 1000000000000u128, 10000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 1209105026582u128; - let exp_put = 1209105026582u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#25 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#25 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0026() { - // S=100.0, K=110.0, T=0.5, r=0.0 - // v0=0.01, kappa=1.0, theta=0.01, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 1000000000000u128, 10000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 62458342955u128; - let exp_put = 10062458342955u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#26 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#26 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0027() { - // S=100.0, K=100.0, T=0.1, r=0.0 - // v0=0.01, kappa=1.0, theta=0.01, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 1000000000000u128, 10000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 986502422738u128; - let exp_put = 986502422738u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#27 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#27 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0028() { - // S=100.0, K=105.0, T=1.0, r=0.0 - // v0=0.01, kappa=1.0, theta=0.04, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 1000000000000u128, - 10000000000u128, 1000000000000u128, 40000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 3475809079502u128; - let exp_put = 8475809079502u128; - let tol = 340000000000u128; // $0.34 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#28 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#28 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0029() { - // S=100.0, K=115.0, T=0.1, r=0.0 - // v0=0.01, kappa=1.0, theta=0.04, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 1000000000000u128, 40000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 0u128; - let exp_put = 15000000000000u128; - let tol = 340000000000u128; // $0.34 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#29 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#29 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0030() { - // S=100.0, K=85.0, T=1.0, r=0.0 - // v0=0.01, kappa=1.0, theta=0.04, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 1000000000000u128, - 10000000000u128, 1000000000000u128, 40000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 15901067620488u128; - let exp_put = 901067620488u128; - let tol = 102941176471u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#30 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#30 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0031() { - // S=100.0, K=100.0, T=0.1, r=0.0 - // v0=0.01, kappa=1.0, theta=0.04, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 1000000000000u128, 40000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 1302253906083u128; - let exp_put = 1302253906083u128; - let tol = 340000000000u128; // $0.34 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#31 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#31 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0032() { - // S=100.0, K=115.0, T=0.5, r=0.0 - // v0=0.01, kappa=1.0, theta=0.04, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 1000000000000u128, 40000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 232823102847u128; - let exp_put = 15232823102847u128; - let tol = 289130434783u128; // $0.29 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#32 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#32 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0033() { - // S=100.0, K=80.0, T=0.1, r=0.0 - // v0=0.01, kappa=1.0, theta=0.04, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 1000000000000u128, 40000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 20001029474924u128; - let exp_put = 1029474924u128; - let tol = 625000000000u128; // $0.62 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#33 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#33 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0034() { - // S=100.0, K=110.0, T=0.5, r=0.0 - // v0=0.01, kappa=1.0, theta=0.09, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 1000000000000u128, 90000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 1285534701322u128; - let exp_put = 11285534701322u128; - let tol = 490000000000u128; // $0.49 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#34 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#34 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0035() { - // S=100.0, K=85.0, T=0.1, r=0.0 - // v0=0.01, kappa=1.0, theta=0.09, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 1000000000000u128, 90000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 15000258648164u128; - let exp_put = 258648164u128; - let tol = 490000000000u128; // $0.49 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#35 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#35 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0036() { - // S=100.0, K=100.0, T=0.5, r=0.0 - // v0=0.01, kappa=1.0, theta=0.09, xi=0.3, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 1000000000000u128, 90000000000u128, 300000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 4423794463564u128; - let exp_put = 4423794463564u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#36 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#36 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0037() { - // S=100.0, K=115.0, T=2.0, r=0.0 - // v0=0.01, kappa=1.0, theta=0.09, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 1000000000000u128, 90000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 4517136176190u128; - let exp_put = 19517136176190u128; - let tol = 489130434783u128; // $0.49 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#37 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#37 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0038() { - // S=100.0, K=85.0, T=0.5, r=0.0 - // v0=0.01, kappa=1.0, theta=0.09, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 1000000000000u128, 90000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 16020744950161u128; - let exp_put = 1020744950161u128; - let tol = 802941176471u128; // $0.80 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#38 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#38 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0039() { - // S=100.0, K=100.0, T=2.0, r=0.0 - // v0=0.01, kappa=1.0, theta=0.09, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 1000000000000u128, 90000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 11413611504566u128; - let exp_put = 11413611504566u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#39 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#39 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0040() { - // S=100.0, K=115.0, T=0.25, r=0.0 - // v0=0.01, kappa=1.0, theta=0.16, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 1000000000000u128, 160000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 166537936981u128; - let exp_put = 15166537936981u128; - let tol = 700000000000u128; // $0.70 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#40 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#40 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0041() { - // S=100.0, K=80.0, T=2.0, r=0.0 - // v0=0.01, kappa=1.0, theta=0.16, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 1000000000000u128, 160000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 27531780213383u128; - let exp_put = 7531780213383u128; - let tol = 125000000000u128; // $0.12 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#41 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#41 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0042() { - // S=100.0, K=90.0, T=0.25, r=0.0 - // v0=0.01, kappa=1.0, theta=0.16, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 1000000000000u128, 160000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 10463897732052u128; - let exp_put = 463897732052u128; - let tol = 283333333333u128; // $0.28 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#42 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#42 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0043() { - // S=100.0, K=100.0, T=1.0, r=0.0 - // v0=0.01, kappa=1.0, theta=0.16, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 1000000000000u128, - 10000000000u128, 1000000000000u128, 160000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 9463217277342u128; - let exp_put = 9463217277342u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#43 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#43 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0044() { - // S=100.0, K=110.0, T=0.1, r=0.0 - // v0=0.01, kappa=1.0, theta=0.16, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 1000000000000u128, 160000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 10306704805u128; - let exp_put = 10010306704805u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#44 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#44 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0045() { - // S=100.0, K=120.0, T=0.5, r=0.0 - // v0=0.01, kappa=2.0, theta=0.01, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 2000000000000u128, 10000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 787353719u128; - let exp_put = 20000787353719u128; - let tol = 400000000000u128; // $0.40 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#45 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#45 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0046() { - // S=100.0, K=95.0, T=0.1, r=0.0 - // v0=0.01, kappa=2.0, theta=0.01, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 2000000000000u128, 10000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 5121388057155u128; - let exp_put = 121388057155u128; - let tol = 400000000000u128; // $0.40 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#46 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#46 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0047() { - // S=100.0, K=110.0, T=0.5, r=0.0 - // v0=0.01, kappa=2.0, theta=0.01, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 2000000000000u128, 10000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 2782490942u128; - let exp_put = 10002782490942u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#47 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#47 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0048() { - // S=100.0, K=90.0, T=0.1, r=0.0 - // v0=0.01, kappa=2.0, theta=0.01, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 2000000000000u128, 10000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 10036677482495u128; - let exp_put = 36677482495u128; - let tol = 483333333333u128; // $0.48 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#48 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#48 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0049() { - // S=100.0, K=110.0, T=0.5, r=0.0 - // v0=0.01, kappa=2.0, theta=0.01, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 2000000000000u128, 10000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 406712758634u128; - let exp_put = 10406712758634u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#49 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#49 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0050() { - // S=100.0, K=120.0, T=2.0, r=0.0 - // v0=0.01, kappa=2.0, theta=0.01, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 2000000000000u128, 10000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 526866458396u128; - let exp_put = 20526866458396u128; - let tol = 600000000000u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#50 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#50 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0051() { - // S=100.0, K=115.0, T=0.5, r=0.0 - // v0=0.01, kappa=2.0, theta=0.04, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 2000000000000u128, 40000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 382701794457u128; - let exp_put = 15382701794457u128; - let tol = 490000000000u128; // $0.49 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#51 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#51 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0052() { - // S=100.0, K=90.0, T=0.1, r=0.0 - // v0=0.01, kappa=2.0, theta=0.04, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 2000000000000u128, 40000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 10005400008218u128; - let exp_put = 5400008218u128; - let tol = 490000000000u128; // $0.49 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#52 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#52 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0053() { - // S=100.0, K=100.0, T=0.5, r=0.0 - // v0=0.01, kappa=2.0, theta=0.04, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 2000000000000u128, 40000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 3914108254975u128; - let exp_put = 3914108254975u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#53 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#53 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0054() { - // S=100.0, K=120.0, T=2.0, r=0.0 - // v0=0.01, kappa=2.0, theta=0.04, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 2000000000000u128, 40000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 1983540124941u128; - let exp_put = 21983540124941u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#54 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#54 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0055() { - // S=100.0, K=85.0, T=0.5, r=0.0 - // v0=0.01, kappa=2.0, theta=0.04, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 2000000000000u128, 40000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 15707303988532u128; - let exp_put = 707303988532u128; - let tol = 602941176471u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#55 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#55 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0056() { - // S=100.0, K=105.0, T=2.0, r=0.0 - // v0=0.01, kappa=2.0, theta=0.09, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 2000000000000u128, 90000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 12650817261573u128; - let exp_put = 17650817261573u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#56 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#56 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0057() { - // S=100.0, K=120.0, T=0.25, r=0.0 - // v0=0.01, kappa=2.0, theta=0.09, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 2000000000000u128, 90000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 2706194406u128; - let exp_put = 20002706194406u128; - let tol = 640000000000u128; // $0.64 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#57 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#57 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0058() { - // S=100.0, K=85.0, T=2.0, r=0.0 - // v0=0.01, kappa=2.0, theta=0.09, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 2000000000000u128, 90000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 22464587393111u128; - let exp_put = 7464587393111u128; - let tol = 102941176471u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#58 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#58 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0059() { - // S=100.0, K=100.0, T=0.25, r=0.0 - // v0=0.01, kappa=2.0, theta=0.09, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 2000000000000u128, 90000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 3216803195276u128; - let exp_put = 3216803195276u128; - let tol = 80000000000u128; // $0.08 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#59 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#59 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0060() { - // S=100.0, K=110.0, T=1.0, r=0.0 - // v0=0.01, kappa=2.0, theta=0.09, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 1000000000000u128, - 10000000000u128, 2000000000000u128, 90000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 4989370518224u128; - let exp_put = 14989370518224u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#60 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#60 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0061() { - // S=100.0, K=120.0, T=0.1, r=0.0 - // v0=0.01, kappa=2.0, theta=0.09, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 2000000000000u128, 90000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 487702539u128; - let exp_put = 20000487702539u128; - let tol = 600000000000u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#61 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#61 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0062() { - // S=100.0, K=85.0, T=1.0, r=0.0 - // v0=0.01, kappa=2.0, theta=0.16, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 1000000000000u128, - 10000000000u128, 2000000000000u128, 160000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 20422508690872u128; - let exp_put = 5422508690872u128; - let tol = 850000000000u128; // $0.85 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#62 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#62 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0063() { - // S=100.0, K=100.0, T=0.1, r=0.0 - // v0=0.01, kappa=2.0, theta=0.16, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 2000000000000u128, 160000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 1925325107266u128; - let exp_put = 1925325107266u128; - let tol = 850000000000u128; // $0.85 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#63 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#63 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0064() { - // S=100.0, K=115.0, T=0.5, r=0.0 - // v0=0.01, kappa=2.0, theta=0.16, xi=0.3, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 2000000000000u128, 160000000000u128, 300000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 1952135092022u128; - let exp_put = 16952135092022u128; - let tol = 150000000000u128; // $0.15 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#64 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#64 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0065() { - // S=100.0, K=85.0, T=0.1, r=0.0 - // v0=0.01, kappa=2.0, theta=0.16, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 2000000000000u128, 160000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 15014699509377u128; - let exp_put = 14699509377u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#65 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#65 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0066() { - // S=100.0, K=95.0, T=0.5, r=0.0 - // v0=0.01, kappa=2.0, theta=0.16, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 2000000000000u128, 160000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 9650035367743u128; - let exp_put = 4650035367743u128; - let tol = 750000000000u128; // $0.75 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#66 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#66 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0067() { - // S=100.0, K=105.0, T=2.0, r=0.0 - // v0=0.01, kappa=2.0, theta=0.16, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 2000000000000u128, 160000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 16879042608144u128; - let exp_put = 21879042608144u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#67 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#67 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0068() { - // S=100.0, K=80.0, T=0.5, r=0.0 - // v0=0.01, kappa=3.0, theta=0.01, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 3000000000000u128, 10000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 20002643416593u128; - let exp_put = 2643416593u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#68 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#68 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0069() { - // S=100.0, K=105.0, T=2.0, r=0.0 - // v0=0.01, kappa=3.0, theta=0.01, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 3000000000000u128, 10000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 3392159841233u128; - let exp_put = 8392159841233u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#69 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#69 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0070() { - // S=100.0, K=120.0, T=0.25, r=0.0 - // v0=0.01, kappa=3.0, theta=0.01, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 3000000000000u128, 10000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 848344291u128; - let exp_put = 20000848344291u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#70 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#70 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0071() { - // S=100.0, K=90.0, T=2.0, r=0.0 - // v0=0.01, kappa=3.0, theta=0.01, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 3000000000000u128, 10000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 11964792817183u128; - let exp_put = 1964792817183u128; - let tol = 283333333333u128; // $0.28 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#71 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#71 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0072() { - // S=100.0, K=105.0, T=0.25, r=0.0 - // v0=0.01, kappa=3.0, theta=0.01, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 3000000000000u128, 10000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 201817011081u128; - let exp_put = 5201817011081u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#72 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#72 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0073() { - // S=100.0, K=100.0, T=2.0, r=0.0 - // v0=0.01, kappa=3.0, theta=0.04, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 3000000000000u128, 40000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 10456648183536u128; - let exp_put = 10456648183536u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#73 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#73 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0074() { - // S=100.0, K=115.0, T=0.25, r=0.0 - // v0=0.01, kappa=3.0, theta=0.04, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 3000000000000u128, 40000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 12893351783u128; - let exp_put = 15012893351783u128; - let tol = 640000000000u128; // $0.64 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#74 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#74 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0075() { - // S=100.0, K=80.0, T=2.0, r=0.0 - // v0=0.01, kappa=3.0, theta=0.04, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 3000000000000u128, 40000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 23096672552674u128; - let exp_put = 3096672552674u128; - let tol = 325000000000u128; // $0.33 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#75 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#75 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0076() { - // S=100.0, K=95.0, T=0.25, r=0.0 - // v0=0.01, kappa=3.0, theta=0.04, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 3000000000000u128, 40000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 6115859835684u128; - let exp_put = 1115859835684u128; - let tol = 450000000000u128; // $0.45 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#76 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#76 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0077() { - // S=100.0, K=105.0, T=1.0, r=0.0 - // v0=0.01, kappa=3.0, theta=0.04, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 1000000000000u128, - 10000000000u128, 3000000000000u128, 40000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 4624329075782u128; - let exp_put = 9624329075782u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#77 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#77 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0078() { - // S=100.0, K=115.0, T=0.1, r=0.0 - // v0=0.01, kappa=3.0, theta=0.04, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 3000000000000u128, 40000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 7576847911u128; - let exp_put = 15007576847911u128; - let tol = 589130434783u128; // $0.59 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#78 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#78 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0079() { - // S=100.0, K=80.0, T=1.0, r=0.0 - // v0=0.01, kappa=3.0, theta=0.09, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 1000000000000u128, - 10000000000u128, 3000000000000u128, 90000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 22421589134092u128; - let exp_put = 2421589134092u128; - let tol = 790000000000u128; // $0.79 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#79 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#79 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0080() { - // S=100.0, K=90.0, T=0.1, r=0.0 - // v0=0.01, kappa=3.0, theta=0.09, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 3000000000000u128, 90000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 10023795153900u128; - let exp_put = 23795153900u128; - let tol = 790000000000u128; // $0.79 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#80 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#80 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0081() { - // S=100.0, K=105.0, T=0.5, r=0.0 - // v0=0.01, kappa=3.0, theta=0.09, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 3000000000000u128, 90000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 3944676072766u128; - let exp_put = 8944676072766u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#81 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#81 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0082() { - // S=100.0, K=115.0, T=2.0, r=0.0 - // v0=0.01, kappa=3.0, theta=0.09, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 3000000000000u128, 90000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 8948961913866u128; - let exp_put = 23948961913866u128; - let tol = 289130434783u128; // $0.29 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#82 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#82 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0083() { - // S=100.0, K=80.0, T=0.5, r=0.0 - // v0=0.01, kappa=3.0, theta=0.09, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 3000000000000u128, 90000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 20994648170951u128; - let exp_put = 994648170951u128; - let tol = 625000000000u128; // $0.62 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#83 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#83 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0084() { - // S=100.0, K=90.0, T=2.0, r=0.0 - // v0=0.01, kappa=3.0, theta=0.16, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 3000000000000u128, 160000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 24832398358106u128; - let exp_put = 14832398358106u128; - let tol = 283333333333u128; // $0.28 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#84 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#84 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0085() { - // S=100.0, K=100.0, T=0.25, r=0.0 - // v0=0.01, kappa=3.0, theta=0.16, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 3000000000000u128, 160000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 4610957660322u128; - let exp_put = 4610957660322u128; - let tol = 1000000000000u128; // $1.00 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#85 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#85 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0086() { - // S=100.0, K=110.0, T=1.0, r=0.0 - // v0=0.01, kappa=3.0, theta=0.16, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 1000000000000u128, - 10000000000u128, 3000000000000u128, 160000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 9511262193491u128; - let exp_put = 19511262193491u128; - let tol = 225000000000u128; // $0.22 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#86 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#86 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0087() { - // S=100.0, K=120.0, T=0.1, r=0.0 - // v0=0.01, kappa=3.0, theta=0.16, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 3000000000000u128, 160000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 1337916344u128; - let exp_put = 20001337916344u128; - let tol = 1000000000000u128; // $1.00 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#87 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#87 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0088() { - // S=100.0, K=85.0, T=1.0, r=0.0 - // v0=0.01, kappa=3.0, theta=0.16, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 1000000000000u128, - 10000000000u128, 3000000000000u128, 160000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 21249231211586u128; - let exp_put = 6249231211586u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#88 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#88 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0089() { - // S=100.0, K=95.0, T=0.1, r=0.0 - // v0=0.01, kappa=3.0, theta=0.16, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 3000000000000u128, 160000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 5568924944820u128; - let exp_put = 568924944820u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#89 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#89 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0090() { - // S=100.0, K=105.0, T=0.5, r=0.0 - // v0=0.01, kappa=5.0, theta=0.01, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 5000000000000u128, 10000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 967845746365u128; - let exp_put = 5967845746365u128; - let tol = 850000000000u128; // $0.85 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#90 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#90 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0091() { - // S=100.0, K=120.0, T=2.0, r=0.0 - // v0=0.01, kappa=5.0, theta=0.01, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 5000000000000u128, 10000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 417888596221u128; - let exp_put = 20417888596221u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#91 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#91 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0092() { - // S=100.0, K=85.0, T=0.5, r=0.0 - // v0=0.01, kappa=5.0, theta=0.01, xi=0.3, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 5000000000000u128, 10000000000u128, 300000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 15145763458158u128; - let exp_put = 145763458158u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#92 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#92 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0093() { - // S=100.0, K=95.0, T=2.0, r=0.0 - // v0=0.01, kappa=5.0, theta=0.01, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 5000000000000u128, 10000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 8359545875700u128; - let exp_put = 3359545875700u128; - let tol = 450000000000u128; // $0.45 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#93 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#93 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0094() { - // S=100.0, K=110.0, T=0.25, r=0.0 - // v0=0.01, kappa=5.0, theta=0.01, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 5000000000000u128, 10000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 57619412u128; - let exp_put = 10000057619412u128; - let tol = 750000000000u128; // $0.75 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#94 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#94 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0095() { - // S=100.0, K=95.0, T=2.0, r=0.0 - // v0=0.01, kappa=5.0, theta=0.01, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 5000000000000u128, 10000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 7954132723156u128; - let exp_put = 2954132723156u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#95 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#95 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0096() { - // S=100.0, K=110.0, T=0.25, r=0.0 - // v0=0.01, kappa=5.0, theta=0.04, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 5000000000000u128, 40000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 391131231311u128; - let exp_put = 10391131231311u128; - let tol = 940000000000u128; // $0.94 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#96 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#96 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0097() { - // S=100.0, K=80.0, T=2.0, r=0.0 - // v0=0.01, kappa=5.0, theta=0.04, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 5000000000000u128, 40000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 22889397646742u128; - let exp_put = 2889397646742u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#97 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#97 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0098() { - // S=100.0, K=95.0, T=0.25, r=0.0 - // v0=0.01, kappa=5.0, theta=0.04, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 5000000000000u128, 40000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 6132802198194u128; - let exp_put = 1132802198194u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#98 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#98 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0099() { - // S=100.0, K=115.0, T=1.0, r=0.0 - // v0=0.01, kappa=5.0, theta=0.04, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 1000000000000u128, - 10000000000u128, 5000000000000u128, 40000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 1980287223451u128; - let exp_put = 16980287223451u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#99 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#99 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0100() { - // S=100.0, K=80.0, T=0.25, r=0.0 - // v0=0.01, kappa=5.0, theta=0.04, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 5000000000000u128, 40000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 20084810828321u128; - let exp_put = 84810828321u128; - let tol = 625000000000u128; // $0.62 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#100 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#100 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0101() { - // S=100.0, K=90.0, T=1.0, r=0.0 - // v0=0.01, kappa=5.0, theta=0.09, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 1000000000000u128, - 10000000000u128, 5000000000000u128, 90000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 16082002516306u128; - let exp_put = 6082002516306u128; - let tol = 1090000000000u128; // $1.09 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#101 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#101 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0102() { - // S=100.0, K=110.0, T=0.1, r=0.0 - // v0=0.01, kappa=5.0, theta=0.09, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 5000000000000u128, 90000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 37724419995u128; - let exp_put = 10037724419995u128; - let tol = 1090000000000u128; // $1.09 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#102 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#102 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0103() { - // S=100.0, K=80.0, T=1.0, r=0.0 - // v0=0.01, kappa=5.0, theta=0.09, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 1000000000000u128, - 10000000000u128, 5000000000000u128, 90000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 23116327051182u128; - let exp_put = 3116327051182u128; - let tol = 325000000000u128; // $0.33 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#103 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#103 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0104() { - // S=100.0, K=90.0, T=0.1, r=0.0 - // v0=0.01, kappa=5.0, theta=0.09, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 5000000000000u128, 90000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 10125471175335u128; - let exp_put = 125471175335u128; - let tol = 483333333333u128; // $0.48 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#104 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#104 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0105() { - // S=100.0, K=100.0, T=0.5, r=0.0 - // v0=0.01, kappa=5.0, theta=0.09, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 500000000000u128, - 10000000000u128, 5000000000000u128, 90000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 6819988863432u128; - let exp_put = 6819988863432u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#105 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#105 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0106() { - // S=100.0, K=110.0, T=2.0, r=0.0 - // v0=0.01, kappa=5.0, theta=0.09, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 5000000000000u128, 90000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 11693343261017u128; - let exp_put = 21693343261017u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#106 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#106 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0107() { - // S=100.0, K=120.0, T=0.25, r=0.0 - // v0=0.01, kappa=5.0, theta=0.16, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 5000000000000u128, 160000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 596596852387u128; - let exp_put = 20596596852387u128; - let tol = 1300000000000u128; // $1.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#107 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#107 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0108() { - // S=100.0, K=90.0, T=2.0, r=0.0 - // v0=0.01, kappa=5.0, theta=0.16, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 2000000000000u128, - 10000000000u128, 5000000000000u128, 160000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 25503697008900u128; - let exp_put = 15503697008900u128; - let tol = 375000000000u128; // $0.38 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#108 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#108 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0109() { - // S=100.0, K=100.0, T=0.25, r=0.0 - // v0=0.01, kappa=5.0, theta=0.16, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 250000000000u128, - 10000000000u128, 5000000000000u128, 160000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 5388289965726u128; - let exp_put = 5388289965726u128; - let tol = 375000000000u128; // $0.38 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#109 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#109 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0110() { - // S=100.0, K=110.0, T=1.0, r=0.0 - // v0=0.01, kappa=5.0, theta=0.16, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 1000000000000u128, - 10000000000u128, 5000000000000u128, 160000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 9972019250748u128; - let exp_put = 19972019250748u128; - let tol = 375000000000u128; // $0.38 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#110 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#110 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0111() { - // S=100.0, K=120.0, T=0.1, r=0.0 - // v0=0.01, kappa=5.0, theta=0.16, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 100000000000u128, - 10000000000u128, 5000000000000u128, 160000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 144433089u128; - let exp_put = 20000144433089u128; - let tol = 600000000000u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#111 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#111 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0112() { - // S=100.0, K=85.0, T=1.0, r=0.0 - // v0=0.04, kappa=0.5, theta=0.01, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 500000000000u128, 10000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 17077147074133u128; - let exp_put = 2077147074133u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#112 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#112 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0113() { - // S=100.0, K=100.0, T=0.1, r=0.0 - // v0=0.04, kappa=0.5, theta=0.01, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 500000000000u128, 10000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 2468888851964u128; - let exp_put = 2468888851964u128; - let tol = 265000000000u128; // $0.27 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#113 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#113 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0114() { - // S=100.0, K=110.0, T=0.5, r=0.0 - // v0=0.04, kappa=0.5, theta=0.01, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 500000000000u128, 10000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 1960478887943u128; - let exp_put = 11960478887943u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#114 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#114 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0115() { - // S=100.0, K=120.0, T=2.0, r=0.0 - // v0=0.04, kappa=0.5, theta=0.01, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 500000000000u128, 10000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 2318391529129u128; - let exp_put = 22318391529129u128; - let tol = 100000000000u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#115 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#115 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0116() { - // S=100.0, K=90.0, T=0.5, r=0.0 - // v0=0.04, kappa=0.5, theta=0.01, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 500000000000u128, 10000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 11729017809293u128; - let exp_put = 1729017809293u128; - let tol = 283333333333u128; // $0.28 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#116 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#116 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0117() { - // S=100.0, K=100.0, T=2.0, r=0.0 - // v0=0.04, kappa=0.5, theta=0.01, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 500000000000u128, 10000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 5570388535195u128; - let exp_put = 5570388535195u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#117 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#117 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0118() { - // S=100.0, K=110.0, T=0.25, r=0.0 - // v0=0.04, kappa=0.5, theta=0.04, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 500000000000u128, 40000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 868257474759u128; - let exp_put = 10868257474759u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#118 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#118 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0119() { - // S=100.0, K=120.0, T=1.0, r=0.0 - // v0=0.04, kappa=0.5, theta=0.04, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 500000000000u128, 40000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 1205228433364u128; - let exp_put = 21205228433364u128; - let tol = 100000000000u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#119 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#119 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0120() { - // S=100.0, K=85.0, T=0.25, r=0.0 - // v0=0.04, kappa=0.5, theta=0.04, xi=0.3, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 500000000000u128, 40000000000u128, 300000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 15398776292243u128; - let exp_put = 398776292243u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#120 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#120 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0121() { - // S=100.0, K=100.0, T=1.0, r=0.0 - // v0=0.04, kappa=0.5, theta=0.04, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 500000000000u128, 40000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 6271058219239u128; - let exp_put = 6271058219239u128; - let tol = 450000000000u128; // $0.45 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#121 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#121 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0122() { - // S=100.0, K=110.0, T=0.1, r=0.0 - // v0=0.04, kappa=0.5, theta=0.04, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 500000000000u128, 40000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 2020324602u128; - let exp_put = 10002020324602u128; - let tol = 750000000000u128; // $0.75 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#122 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#122 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0123() { - // S=100.0, K=120.0, T=0.5, r=0.0 - // v0=0.04, kappa=0.5, theta=0.04, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 500000000000u128, 40000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 942340136756u128; - let exp_put = 20942340136756u128; - let tol = 600000000000u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#123 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#123 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0124() { - // S=100.0, K=85.0, T=0.1, r=0.0 - // v0=0.04, kappa=0.5, theta=0.09, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 500000000000u128, 90000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 15010575514772u128; - let exp_put = 10575514772u128; - let tol = 325000000000u128; // $0.33 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#124 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#124 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0125() { - // S=100.0, K=95.0, T=0.5, r=0.0 - // v0=0.04, kappa=0.5, theta=0.09, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 500000000000u128, 90000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 8699345964140u128; - let exp_put = 3699345964140u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#125 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#125 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0126() { - // S=100.0, K=105.0, T=2.0, r=0.0 - // v0=0.04, kappa=0.5, theta=0.09, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 500000000000u128, 90000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 10210638294363u128; - let exp_put = 15210638294363u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#126 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#126 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0127() { - // S=100.0, K=115.0, T=0.25, r=0.0 - // v0=0.04, kappa=0.5, theta=0.09, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 500000000000u128, 90000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 229901611232u128; - let exp_put = 15229901611232u128; - let tol = 289130434783u128; // $0.29 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#127 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#127 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0128() { - // S=100.0, K=80.0, T=2.0, r=0.0 - // v0=0.04, kappa=0.5, theta=0.09, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 500000000000u128, 90000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 24296619193437u128; - let exp_put = 4296619193437u128; - let tol = 625000000000u128; // $0.62 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#128 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#128 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0129() { - // S=100.0, K=90.0, T=0.25, r=0.0 - // v0=0.04, kappa=0.5, theta=0.16, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 500000000000u128, 160000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 10986281841679u128; - let exp_put = 986281841679u128; - let tol = 535000000000u128; // $0.53 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#129 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#129 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0130() { - // S=100.0, K=100.0, T=1.0, r=0.0 - // v0=0.04, kappa=0.5, theta=0.16, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 500000000000u128, 160000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 9867310807067u128; - let exp_put = 9867310807067u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#130 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#130 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0131() { - // S=100.0, K=110.0, T=0.1, r=0.0 - // v0=0.04, kappa=0.5, theta=0.16, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 500000000000u128, 160000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 84160584600u128; - let exp_put = 10084160584600u128; - let tol = 535000000000u128; // $0.53 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#131 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#131 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0132() { - // S=100.0, K=120.0, T=0.5, r=0.0 - // v0=0.04, kappa=0.5, theta=0.16, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 500000000000u128, 160000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 1221394926798u128; - let exp_put = 21221394926798u128; - let tol = 100000000000u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#132 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#132 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0133() { - // S=100.0, K=85.0, T=0.1, r=0.0 - // v0=0.04, kappa=0.5, theta=0.16, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 500000000000u128, 160000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 15023403222136u128; - let exp_put = 23403222136u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#133 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#133 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0134() { - // S=100.0, K=95.0, T=0.5, r=0.0 - // v0=0.04, kappa=0.5, theta=0.16, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 500000000000u128, 160000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 8628132213538u128; - let exp_put = 3628132213538u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#134 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#134 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0135() { - // S=100.0, K=105.0, T=2.0, r=0.0 - // v0=0.04, kappa=1.0, theta=0.01, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 1000000000000u128, 10000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 6155808936625u128; - let exp_put = 11155808936625u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#135 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#135 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0136() { - // S=100.0, K=120.0, T=0.25, r=0.0 - // v0=0.04, kappa=1.0, theta=0.01, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 1000000000000u128, 10000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 55735008126u128; - let exp_put = 20055735008126u128; - let tol = 340000000000u128; // $0.34 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#136 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#136 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0137() { - // S=100.0, K=85.0, T=2.0, r=0.0 - // v0=0.04, kappa=1.0, theta=0.01, xi=0.3, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 1000000000000u128, 10000000000u128, 300000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 17992591656508u128; - let exp_put = 2992591656508u128; - let tol = 102941176471u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#137 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#137 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0138() { - // S=100.0, K=95.0, T=0.25, r=0.0 - // v0=0.04, kappa=1.0, theta=0.01, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 1000000000000u128, 10000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 6902604209626u128; - let exp_put = 1902604209626u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#138 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#138 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0139() { - // S=100.0, K=105.0, T=1.0, r=0.0 - // v0=0.04, kappa=1.0, theta=0.01, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 1000000000000u128, 10000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 1101339853969u128; - let exp_put = 6101339853969u128; - let tol = 750000000000u128; // $0.75 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#139 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#139 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0140() { - // S=100.0, K=115.0, T=0.1, r=0.0 - // v0=0.04, kappa=1.0, theta=0.04, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 1000000000000u128, 40000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 15437995764u128; - let exp_put = 15015437995764u128; - let tol = 289130434783u128; // $0.29 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#140 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#140 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0141() { - // S=100.0, K=80.0, T=1.0, r=0.0 - // v0=0.04, kappa=1.0, theta=0.04, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 1000000000000u128, 40000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 21190739397855u128; - let exp_put = 1190739397855u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#141 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#141 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0142() { - // S=100.0, K=90.0, T=0.1, r=0.0 - // v0=0.04, kappa=1.0, theta=0.04, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 1000000000000u128, 40000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 10119036864084u128; - let exp_put = 119036864084u128; - let tol = 283333333333u128; // $0.28 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#142 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#142 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0143() { - // S=100.0, K=100.0, T=0.5, r=0.0 - // v0=0.04, kappa=1.0, theta=0.04, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 1000000000000u128, 40000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 5424160362750u128; - let exp_put = 5424160362750u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#143 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#143 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0144() { - // S=100.0, K=110.0, T=2.0, r=0.0 - // v0=0.04, kappa=1.0, theta=0.04, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 1000000000000u128, 40000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 5243412863777u128; - let exp_put = 15243412863777u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#144 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#144 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0145() { - // S=100.0, K=120.0, T=0.25, r=0.0 - // v0=0.04, kappa=1.0, theta=0.04, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 1000000000000u128, 40000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 77730186525u128; - let exp_put = 20077730186525u128; - let tol = 600000000000u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#145 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#145 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0146() { - // S=100.0, K=85.0, T=2.0, r=0.0 - // v0=0.04, kappa=1.0, theta=0.09, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 1000000000000u128, 90000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 22452186811313u128; - let exp_put = 7452186811313u128; - let tol = 102941176471u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#146 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#146 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0147() { - // S=100.0, K=95.0, T=0.25, r=0.0 - // v0=0.04, kappa=1.0, theta=0.09, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 1000000000000u128, 90000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 7211247648713u128; - let exp_put = 2211247648713u128; - let tol = 400000000000u128; // $0.40 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#147 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#147 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0148() { - // S=100.0, K=105.0, T=1.0, r=0.0 - // v0=0.04, kappa=1.0, theta=0.09, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 1000000000000u128, 90000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 6665610161132u128; - let exp_put = 11665610161132u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#148 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#148 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0149() { - // S=100.0, K=115.0, T=0.1, r=0.0 - // v0=0.04, kappa=1.0, theta=0.09, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 1000000000000u128, 90000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 74950646u128; - let exp_put = 15000074950646u128; - let tol = 489130434783u128; // $0.49 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#149 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#149 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0150() { - // S=100.0, K=80.0, T=1.0, r=0.0 - // v0=0.04, kappa=1.0, theta=0.09, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 1000000000000u128, 90000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 22026941060981u128; - let exp_put = 2026941060981u128; - let tol = 325000000000u128; // $0.33 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#150 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#150 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0151() { - // S=100.0, K=90.0, T=0.1, r=0.0 - // v0=0.04, kappa=1.0, theta=0.09, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 1000000000000u128, 90000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 10180882145093u128; - let exp_put = 180882145093u128; - let tol = 583333333333u128; // $0.58 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#151 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#151 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0152() { - // S=100.0, K=100.0, T=0.5, r=0.0 - // v0=0.04, kappa=1.0, theta=0.16, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 1000000000000u128, 160000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 7176899386439u128; - let exp_put = 7176899386439u128; - let tol = 610000000000u128; // $0.61 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#152 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#152 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0153() { - // S=100.0, K=110.0, T=2.0, r=0.0 - // v0=0.04, kappa=1.0, theta=0.16, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 1000000000000u128, 160000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 14117464595916u128; - let exp_put = 24117464595916u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#153 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#153 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0154() { - // S=100.0, K=120.0, T=0.25, r=0.0 - // v0=0.04, kappa=1.0, theta=0.16, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 1000000000000u128, 160000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 184708053627u128; - let exp_put = 20184708053627u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#154 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#154 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0155() { - // S=100.0, K=85.0, T=2.0, r=0.0 - // v0=0.04, kappa=1.0, theta=0.16, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 1000000000000u128, 160000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 25114529880105u128; - let exp_put = 10114529880105u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#155 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#155 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0156() { - // S=100.0, K=100.0, T=0.25, r=0.0 - // v0=0.04, kappa=1.0, theta=0.16, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 1000000000000u128, 160000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 4157963043754u128; - let exp_put = 4157963043754u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#156 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#156 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0157() { - // S=100.0, K=110.0, T=1.0, r=0.0 - // v0=0.04, kappa=2.0, theta=0.01, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 2000000000000u128, 10000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 2163212147938u128; - let exp_put = 12163212147938u128; - let tol = 490000000000u128; // $0.49 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#157 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#157 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0158() { - // S=100.0, K=80.0, T=0.25, r=0.0 - // v0=0.04, kappa=2.0, theta=0.01, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 2000000000000u128, 10000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 20082409779811u128; - let exp_put = 82409779811u128; - let tol = 490000000000u128; // $0.49 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#158 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#158 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0159() { - // S=100.0, K=90.0, T=1.0, r=0.0 - // v0=0.04, kappa=2.0, theta=0.01, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 2000000000000u128, 10000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 12016295241642u128; - let exp_put = 2016295241642u128; - let tol = 83333333333u128; // $0.08 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#159 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#159 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0160() { - // S=100.0, K=100.0, T=0.1, r=0.0 - // v0=0.04, kappa=2.0, theta=0.01, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 2000000000000u128, 10000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 2395469288186u128; - let exp_put = 2395469288186u128; - let tol = 490000000000u128; // $0.49 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#160 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#160 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0161() { - // S=100.0, K=110.0, T=0.5, r=0.0 - // v0=0.04, kappa=2.0, theta=0.01, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 2000000000000u128, 10000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 1120478995084u128; - let exp_put = 11120478995084u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#161 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#161 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0162() { - // S=100.0, K=120.0, T=2.0, r=0.0 - // v0=0.04, kappa=2.0, theta=0.01, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 2000000000000u128, 10000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 653091967667u128; - let exp_put = 20653091967667u128; - let tol = 600000000000u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#162 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#162 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0163() { - // S=100.0, K=85.0, T=0.5, r=0.0 - // v0=0.04, kappa=2.0, theta=0.04, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 2000000000000u128, 40000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 15897933898405u128; - let exp_put = 897933898405u128; - let tol = 400000000000u128; // $0.40 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#163 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#163 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0164() { - // S=100.0, K=95.0, T=2.0, r=0.0 - // v0=0.04, kappa=2.0, theta=0.04, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 2000000000000u128, 40000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 13549607179924u128; - let exp_put = 8549607179924u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#164 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#164 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0165() { - // S=100.0, K=110.0, T=0.25, r=0.0 - // v0=0.04, kappa=2.0, theta=0.04, xi=0.3, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 2000000000000u128, 40000000000u128, 300000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 628683276152u128; - let exp_put = 10628683276152u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#165 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#165 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0166() { - // S=100.0, K=120.0, T=1.0, r=0.0 - // v0=0.04, kappa=2.0, theta=0.04, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 2000000000000u128, 40000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 332315907025u128; - let exp_put = 20332315907025u128; - let tol = 500000000000u128; // $0.50 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#166 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#166 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0167() { - // S=100.0, K=85.0, T=0.25, r=0.0 - // v0=0.04, kappa=2.0, theta=0.04, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 2000000000000u128, 40000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 15691663229526u128; - let exp_put = 691663229526u128; - let tol = 802941176471u128; // $0.80 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#167 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#167 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0168() { - // S=100.0, K=100.0, T=1.0, r=0.0 - // v0=0.04, kappa=2.0, theta=0.04, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 2000000000000u128, 40000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 6965405871923u128; - let exp_put = 6965405871923u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#168 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#168 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0169() { - // S=100.0, K=110.0, T=0.1, r=0.0 - // v0=0.04, kappa=2.0, theta=0.09, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 2000000000000u128, 90000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 235919899760u128; - let exp_put = 10235919899760u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#169 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#169 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0170() { - // S=100.0, K=120.0, T=0.5, r=0.0 - // v0=0.04, kappa=2.0, theta=0.09, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 2000000000000u128, 90000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 1219336488589u128; - let exp_put = 21219336488589u128; - let tol = 100000000000u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#170 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#170 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0171() { - // S=100.0, K=85.0, T=0.1, r=0.0 - // v0=0.04, kappa=2.0, theta=0.09, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 2000000000000u128, 90000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 15028129838453u128; - let exp_put = 28129838453u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#171 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#171 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0172() { - // S=100.0, K=95.0, T=0.5, r=0.0 - // v0=0.04, kappa=2.0, theta=0.09, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 2000000000000u128, 90000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 9359365009677u128; - let exp_put = 4359365009677u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#172 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#172 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0173() { - // S=100.0, K=105.0, T=2.0, r=0.0 - // v0=0.04, kappa=2.0, theta=0.09, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 2000000000000u128, 90000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 11331960813057u128; - let exp_put = 16331960813057u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#173 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#173 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0174() { - // S=100.0, K=115.0, T=0.25, r=0.0 - // v0=0.04, kappa=2.0, theta=0.16, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 2000000000000u128, 160000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 859772256267u128; - let exp_put = 15859772256267u128; - let tol = 760000000000u128; // $0.76 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#174 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#174 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0175() { - // S=100.0, K=80.0, T=2.0, r=0.0 - // v0=0.04, kappa=2.0, theta=0.16, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 2000000000000u128, 160000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 29920218574627u128; - let exp_put = 9920218574627u128; - let tol = 325000000000u128; // $0.33 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#175 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#175 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0176() { - // S=100.0, K=90.0, T=0.25, r=0.0 - // v0=0.04, kappa=2.0, theta=0.16, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 2000000000000u128, 160000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 11618227498124u128; - let exp_put = 1618227498124u128; - let tol = 283333333333u128; // $0.28 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#176 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#176 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0177() { - // S=100.0, K=100.0, T=1.0, r=0.0 - // v0=0.04, kappa=2.0, theta=0.16, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 2000000000000u128, 160000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 12951449326905u128; - let exp_put = 12951449326905u128; - let tol = 120000000000u128; // $0.12 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#177 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#177 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0178() { - // S=100.0, K=110.0, T=0.1, r=0.0 - // v0=0.04, kappa=2.0, theta=0.16, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 2000000000000u128, 160000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 323922112739u128; - let exp_put = 10323922112739u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#178 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#178 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0179() { - // S=100.0, K=120.0, T=0.5, r=0.0 - // v0=0.04, kappa=2.0, theta=0.16, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 2000000000000u128, 160000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 1744533240439u128; - let exp_put = 21744533240439u128; - let tol = 600000000000u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#179 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#179 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0180() { - // S=100.0, K=85.0, T=0.1, r=0.0 - // v0=0.04, kappa=3.0, theta=0.01, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 3000000000000u128, 10000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 15007419873350u128; - let exp_put = 7419873350u128; - let tol = 640000000000u128; // $0.64 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#180 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#180 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0181() { - // S=100.0, K=105.0, T=0.5, r=0.0 - // v0=0.04, kappa=3.0, theta=0.01, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 3000000000000u128, 10000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 2320358882645u128; - let exp_put = 7320358882645u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#181 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#181 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0182() { - // S=100.0, K=115.0, T=2.0, r=0.0 - // v0=0.04, kappa=3.0, theta=0.01, xi=0.3, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 3000000000000u128, 10000000000u128, 300000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 1293753186250u128; - let exp_put = 16293753186250u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#182 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#182 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0183() { - // S=100.0, K=80.0, T=0.5, r=0.0 - // v0=0.04, kappa=3.0, theta=0.01, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 3000000000000u128, 10000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 20442175972707u128; - let exp_put = 442175972707u128; - let tol = 325000000000u128; // $0.33 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#183 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#183 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0184() { - // S=100.0, K=90.0, T=2.0, r=0.0 - // v0=0.04, kappa=3.0, theta=0.01, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 3000000000000u128, 10000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 12765301229591u128; - let exp_put = 2765301229591u128; - let tol = 783333333333u128; // $0.78 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#184 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#184 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0185() { - // S=100.0, K=100.0, T=0.25, r=0.0 - // v0=0.04, kappa=3.0, theta=0.04, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 3000000000000u128, 40000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 3967469622134u128; - let exp_put = 3967469622134u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#185 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#185 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0186() { - // S=100.0, K=110.0, T=1.0, r=0.0 - // v0=0.04, kappa=3.0, theta=0.04, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 3000000000000u128, 40000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 4281331754373u128; - let exp_put = 14281331754373u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#186 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#186 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0187() { - // S=100.0, K=80.0, T=0.25, r=0.0 - // v0=0.04, kappa=3.0, theta=0.04, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 3000000000000u128, 40000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 20046444660584u128; - let exp_put = 46444660584u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#187 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#187 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0188() { - // S=100.0, K=90.0, T=1.0, r=0.0 - // v0=0.04, kappa=3.0, theta=0.04, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 3000000000000u128, 40000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 13642069827986u128; - let exp_put = 3642069827986u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#188 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#188 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0189() { - // S=100.0, K=100.0, T=0.1, r=0.0 - // v0=0.04, kappa=3.0, theta=0.04, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 3000000000000u128, 40000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 2450256059438u128; - let exp_put = 2450256059438u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#189 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#189 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0190() { - // S=100.0, K=110.0, T=0.5, r=0.0 - // v0=0.04, kappa=3.0, theta=0.04, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 3000000000000u128, 40000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 1306787469521u128; - let exp_put = 11306787469521u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#190 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#190 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0191() { - // S=100.0, K=120.0, T=2.0, r=0.0 - // v0=0.04, kappa=3.0, theta=0.09, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 3000000000000u128, 90000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 9097505077201u128; - let exp_put = 29097505077201u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#191 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#191 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0192() { - // S=100.0, K=85.0, T=0.5, r=0.0 - // v0=0.04, kappa=3.0, theta=0.09, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 3000000000000u128, 90000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 16810284206541u128; - let exp_put = 1810284206541u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#192 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#192 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0193() { - // S=100.0, K=95.0, T=2.0, r=0.0 - // v0=0.04, kappa=3.0, theta=0.09, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 3000000000000u128, 90000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 18006785711461u128; - let exp_put = 13006785711461u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#193 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#193 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0194() { - // S=100.0, K=105.0, T=0.25, r=0.0 - // v0=0.04, kappa=3.0, theta=0.09, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 3000000000000u128, 90000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 2188963889894u128; - let exp_put = 7188963889894u128; - let tol = 450000000000u128; // $0.45 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#194 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#194 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0195() { - // S=100.0, K=115.0, T=1.0, r=0.0 - // v0=0.04, kappa=3.0, theta=0.09, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 3000000000000u128, 90000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 5472504680855u128; - let exp_put = 20472504680855u128; - let tol = 289130434783u128; // $0.29 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#195 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#195 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0196() { - // S=100.0, K=80.0, T=0.25, r=0.0 - // v0=0.04, kappa=3.0, theta=0.09, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 3000000000000u128, 90000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 20195064064743u128; - let exp_put = 195064064743u128; - let tol = 625000000000u128; // $0.62 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#196 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#196 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0197() { - // S=100.0, K=90.0, T=1.0, r=0.0 - // v0=0.04, kappa=3.0, theta=0.16, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 3000000000000u128, 160000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 18766033299134u128; - let exp_put = 8766033299134u128; - let tol = 910000000000u128; // $0.91 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#197 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#197 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0198() { - // S=100.0, K=100.0, T=0.1, r=0.0 - // v0=0.04, kappa=3.0, theta=0.16, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 3000000000000u128, 160000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 2959652489784u128; - let exp_put = 2959652489784u128; - let tol = 910000000000u128; // $0.91 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#198 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#198 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0199() { - // S=100.0, K=110.0, T=0.5, r=0.0 - // v0=0.04, kappa=3.0, theta=0.16, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 3000000000000u128, 160000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 4808434462763u128; - let exp_put = 14808434462763u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#199 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#199 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0200() { - // S=100.0, K=120.0, T=2.0, r=0.0 - // v0=0.04, kappa=3.0, theta=0.16, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 3000000000000u128, 160000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 12926482553838u128; - let exp_put = 32926482553838u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#200 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#200 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0201() { - // S=100.0, K=85.0, T=0.5, r=0.0 - // v0=0.04, kappa=3.0, theta=0.16, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 3000000000000u128, 160000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 18130439546874u128; - let exp_put = 3130439546874u128; - let tol = 602941176471u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#201 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#201 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0202() { - // S=100.0, K=95.0, T=2.0, r=0.0 - // v0=0.04, kappa=5.0, theta=0.01, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 5000000000000u128, 10000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 9132838632813u128; - let exp_put = 4132838632813u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#202 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#202 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0203() { - // S=100.0, K=120.0, T=0.25, r=0.0 - // v0=0.04, kappa=5.0, theta=0.01, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 5000000000000u128, 10000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 1525798969u128; - let exp_put = 20001525798969u128; - let tol = 940000000000u128; // $0.94 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#203 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#203 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0204() { - // S=100.0, K=85.0, T=2.0, r=0.0 - // v0=0.04, kappa=5.0, theta=0.01, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 5000000000000u128, 10000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 16217705206080u128; - let exp_put = 1217705206080u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#204 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#204 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0205() { - // S=100.0, K=95.0, T=0.25, r=0.0 - // v0=0.04, kappa=5.0, theta=0.01, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 5000000000000u128, 10000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 6274932844769u128; - let exp_put = 1274932844769u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#205 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#205 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0206() { - // S=100.0, K=105.0, T=1.0, r=0.0 - // v0=0.04, kappa=5.0, theta=0.01, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 5000000000000u128, 10000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 2632914285423u128; - let exp_put = 7632914285423u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#206 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#206 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0207() { - // S=100.0, K=120.0, T=0.1, r=0.0 - // v0=0.04, kappa=5.0, theta=0.01, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 5000000000000u128, 10000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 3365779286u128; - let exp_put = 20003365779286u128; - let tol = 600000000000u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#207 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#207 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0208() { - // S=100.0, K=85.0, T=1.0, r=0.0 - // v0=0.04, kappa=5.0, theta=0.04, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 5000000000000u128, 40000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 17232435594620u128; - let exp_put = 2232435594620u128; - let tol = 850000000000u128; // $0.85 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#208 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#208 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0209() { - // S=100.0, K=100.0, T=0.1, r=0.0 - // v0=0.04, kappa=5.0, theta=0.04, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 5000000000000u128, 40000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 2495690564752u128; - let exp_put = 2495690564752u128; - let tol = 850000000000u128; // $0.85 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#209 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#209 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0210() { - // S=100.0, K=115.0, T=0.5, r=0.0 - // v0=0.04, kappa=5.0, theta=0.04, xi=0.3, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 5000000000000u128, 40000000000u128, 300000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 902835057232u128; - let exp_put = 15902835057232u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#210 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#210 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0211() { - // S=100.0, K=85.0, T=0.1, r=0.0 - // v0=0.04, kappa=5.0, theta=0.04, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 5000000000000u128, 40000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 15051599916178u128; - let exp_put = 51599916178u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#211 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#211 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0212() { - // S=100.0, K=95.0, T=0.5, r=0.0 - // v0=0.04, kappa=5.0, theta=0.04, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 5000000000000u128, 40000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 8332712497439u128; - let exp_put = 3332712497439u128; - let tol = 750000000000u128; // $0.75 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#212 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#212 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0213() { - // S=100.0, K=105.0, T=2.0, r=0.0 - // v0=0.04, kappa=5.0, theta=0.04, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 5000000000000u128, 40000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 8865675979308u128; - let exp_put = 13865675979308u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#213 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#213 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0214() { - // S=100.0, K=120.0, T=0.25, r=0.0 - // v0=0.04, kappa=5.0, theta=0.09, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 5000000000000u128, 90000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 422840090236u128; - let exp_put = 20422840090236u128; - let tol = 1000000000000u128; // $1.00 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#214 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#214 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0215() { - // S=100.0, K=85.0, T=2.0, r=0.0 - // v0=0.04, kappa=5.0, theta=0.09, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 5000000000000u128, 90000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 23767360831968u128; - let exp_put = 8767360831968u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#215 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#215 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0216() { - // S=100.0, K=95.0, T=0.25, r=0.0 - // v0=0.04, kappa=5.0, theta=0.09, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 5000000000000u128, 90000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 7739501263405u128; - let exp_put = 2739501263405u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#216 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#216 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0217() { - // S=100.0, K=105.0, T=1.0, r=0.0 - // v0=0.04, kappa=5.0, theta=0.09, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 5000000000000u128, 90000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 8832806075122u128; - let exp_put = 13832806075122u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#217 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#217 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0218() { - // S=100.0, K=115.0, T=0.1, r=0.0 - // v0=0.04, kappa=5.0, theta=0.09, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 5000000000000u128, 90000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 26602517889u128; - let exp_put = 15026602517889u128; - let tol = 589130434783u128; // $0.59 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#218 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#218 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0219() { - // S=100.0, K=80.0, T=1.0, r=0.0 - // v0=0.04, kappa=5.0, theta=0.16, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 1000000000000u128, - 40000000000u128, 5000000000000u128, 160000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 25536649181632u128; - let exp_put = 5536649181632u128; - let tol = 1210000000000u128; // $1.21 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#219 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#219 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0220() { - // S=100.0, K=90.0, T=0.1, r=0.0 - // v0=0.04, kappa=5.0, theta=0.16, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 100000000000u128, - 40000000000u128, 5000000000000u128, 160000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 10389642489025u128; - let exp_put = 389642489025u128; - let tol = 1210000000000u128; // $1.21 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#220 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#220 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0221() { - // S=100.0, K=100.0, T=0.5, r=0.0 - // v0=0.04, kappa=5.0, theta=0.16, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 500000000000u128, - 40000000000u128, 5000000000000u128, 160000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 9453043140215u128; - let exp_put = 9453043140215u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#221 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#221 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0222() { - // S=100.0, K=110.0, T=2.0, r=0.0 - // v0=0.04, kappa=5.0, theta=0.16, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 5000000000000u128, 160000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 17819386147559u128; - let exp_put = 27819386147559u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#222 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#222 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0223() { - // S=100.0, K=120.0, T=0.25, r=0.0 - // v0=0.04, kappa=5.0, theta=0.16, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 250000000000u128, - 40000000000u128, 5000000000000u128, 160000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 925353213673u128; - let exp_put = 20925353213673u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#223 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#223 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0224() { - // S=100.0, K=90.0, T=2.0, r=0.0 - // v0=0.04, kappa=5.0, theta=0.16, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 2000000000000u128, - 40000000000u128, 5000000000000u128, 160000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 25494207623440u128; - let exp_put = 15494207623440u128; - let tol = 583333333333u128; // $0.58 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#224 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#224 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0225() { - // S=100.0, K=100.0, T=0.25, r=0.0 - // v0=0.09, kappa=0.5, theta=0.01, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 500000000000u128, 10000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 5797938827614u128; - let exp_put = 5797938827614u128; - let tol = 415000000000u128; // $0.41 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#225 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#225 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0226() { - // S=100.0, K=110.0, T=1.0, r=0.0 - // v0=0.09, kappa=0.5, theta=0.01, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 500000000000u128, 10000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 6361856522311u128; - let exp_put = 16361856522311u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#226 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#226 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0227() { - // S=100.0, K=120.0, T=0.1, r=0.0 - // v0=0.09, kappa=0.5, theta=0.01, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 500000000000u128, 10000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 59381698251u128; - let exp_put = 20059381698251u128; - let tol = 415000000000u128; // $0.41 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#227 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#227 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0228() { - // S=100.0, K=85.0, T=1.0, r=0.0 - // v0=0.09, kappa=0.5, theta=0.01, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 500000000000u128, 10000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 19486084027341u128; - let exp_put = 4486084027341u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#228 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#228 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0229() { - // S=100.0, K=95.0, T=0.1, r=0.0 - // v0=0.09, kappa=0.5, theta=0.01, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 500000000000u128, 10000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 6812220084864u128; - let exp_put = 1812220084864u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#229 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#229 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0230() { - // S=100.0, K=105.0, T=0.5, r=0.0 - // v0=0.09, kappa=0.5, theta=0.04, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 500000000000u128, 40000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 5944057806162u128; - let exp_put = 10944057806162u128; - let tol = 325000000000u128; // $0.33 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#230 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#230 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0231() { - // S=100.0, K=115.0, T=2.0, r=0.0 - // v0=0.09, kappa=0.5, theta=0.04, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 500000000000u128, 40000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 9621366957411u128; - let exp_put = 24621366957411u128; - let tol = 89130434783u128; // $0.09 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#231 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#231 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0232() { - // S=100.0, K=80.0, T=0.5, r=0.0 - // v0=0.09, kappa=0.5, theta=0.04, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 500000000000u128, 40000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 21288665861476u128; - let exp_put = 1288665861476u128; - let tol = 125000000000u128; // $0.12 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#232 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#232 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0233() { - // S=100.0, K=90.0, T=2.0, r=0.0 - // v0=0.09, kappa=0.5, theta=0.04, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 500000000000u128, 40000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 19234013635066u128; - let exp_put = 9234013635066u128; - let tol = 83333333333u128; // $0.08 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#233 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#233 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0234() { - // S=100.0, K=100.0, T=0.25, r=0.0 - // v0=0.09, kappa=0.5, theta=0.04, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 500000000000u128, 40000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 5684332635047u128; - let exp_put = 5684332635047u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#234 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#234 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0235() { - // S=100.0, K=110.0, T=1.0, r=0.0 - // v0=0.09, kappa=0.5, theta=0.04, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 500000000000u128, 40000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 4336665236963u128; - let exp_put = 14336665236963u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#235 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#235 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0236() { - // S=100.0, K=120.0, T=0.1, r=0.0 - // v0=0.09, kappa=0.5, theta=0.09, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 500000000000u128, 90000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 88799885950u128; - let exp_put = 20088799885950u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#236 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#236 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0237() { - // S=100.0, K=90.0, T=1.0, r=0.0 - // v0=0.09, kappa=0.5, theta=0.09, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 500000000000u128, 90000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 17097423323581u128; - let exp_put = 7097423323581u128; - let tol = 83333333333u128; // $0.08 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#237 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#237 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0238() { - // S=100.0, K=100.0, T=0.1, r=0.0 - // v0=0.09, kappa=0.5, theta=0.09, xi=0.3, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 500000000000u128, 90000000000u128, 300000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 3734545075507u128; - let exp_put = 3734545075507u128; - let tol = 175000000000u128; // $0.17 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#238 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#238 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0239() { - // S=100.0, K=110.0, T=0.5, r=0.0 - // v0=0.09, kappa=0.5, theta=0.09, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 500000000000u128, 90000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 3174410554080u128; - let exp_put = 13174410554080u128; - let tol = 450000000000u128; // $0.45 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#239 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#239 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0240() { - // S=100.0, K=120.0, T=2.0, r=0.0 - // v0=0.09, kappa=0.5, theta=0.09, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 500000000000u128, 90000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 8891983151663u128; - let exp_put = 28891983151663u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#240 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#240 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0241() { - // S=100.0, K=85.0, T=0.5, r=0.0 - // v0=0.09, kappa=0.5, theta=0.09, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 500000000000u128, 90000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 17302762377119u128; - let exp_put = 2302762377119u128; - let tol = 602941176471u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#241 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#241 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0242() { - // S=100.0, K=95.0, T=2.0, r=0.0 - // v0=0.09, kappa=0.5, theta=0.16, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 500000000000u128, 160000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 21024707492639u128; - let exp_put = 16024707492639u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#242 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#242 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0243() { - // S=100.0, K=110.0, T=0.25, r=0.0 - // v0=0.09, kappa=0.5, theta=0.16, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 500000000000u128, 160000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 2512240584579u128; - let exp_put = 12512240584579u128; - let tol = 385000000000u128; // $0.39 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#243 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#243 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0244() { - // S=100.0, K=120.0, T=1.0, r=0.0 - // v0=0.09, kappa=0.5, theta=0.16, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 500000000000u128, 160000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 5277219947450u128; - let exp_put = 25277219947450u128; - let tol = 100000000000u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#244 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#244 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0245() { - // S=100.0, K=85.0, T=0.25, r=0.0 - // v0=0.09, kappa=0.5, theta=0.16, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 500000000000u128, 160000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 16314765695511u128; - let exp_put = 1314765695511u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#245 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#245 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0246() { - // S=100.0, K=95.0, T=1.0, r=0.0 - // v0=0.09, kappa=0.5, theta=0.16, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 500000000000u128, 160000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 13507656978845u128; - let exp_put = 8507656978845u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#246 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#246 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0247() { - // S=100.0, K=110.0, T=0.1, r=0.0 - // v0=0.09, kappa=1.0, theta=0.01, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 1000000000000u128, 10000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 709107010477u128; - let exp_put = 10709107010477u128; - let tol = 490000000000u128; // $0.49 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#247 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#247 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0248() { - // S=100.0, K=120.0, T=0.5, r=0.0 - // v0=0.09, kappa=1.0, theta=0.01, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 1000000000000u128, 10000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 1239896837000u128; - let exp_put = 21239896837000u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#248 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#248 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0249() { - // S=100.0, K=85.0, T=0.1, r=0.0 - // v0=0.09, kappa=1.0, theta=0.01, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 1000000000000u128, 10000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 15223158469825u128; - let exp_put = 223158469825u128; - let tol = 490000000000u128; // $0.49 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#249 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#249 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0250() { - // S=100.0, K=95.0, T=0.5, r=0.0 - // v0=0.09, kappa=1.0, theta=0.01, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 1000000000000u128, 10000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 10045582832431u128; - let exp_put = 5045582832431u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#250 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#250 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0251() { - // S=100.0, K=105.0, T=2.0, r=0.0 - // v0=0.09, kappa=1.0, theta=0.01, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 1000000000000u128, 10000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 7861100704981u128; - let exp_put = 12861100704981u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#251 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#251 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0252() { - // S=100.0, K=115.0, T=0.25, r=0.0 - // v0=0.09, kappa=1.0, theta=0.01, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 1000000000000u128, 10000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 979525338241u128; - let exp_put = 15979525338241u128; - let tol = 589130434783u128; // $0.59 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#252 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#252 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0253() { - // S=100.0, K=80.0, T=2.0, r=0.0 - // v0=0.09, kappa=1.0, theta=0.04, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 1000000000000u128, 40000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 25115208859463u128; - let exp_put = 5115208859463u128; - let tol = 125000000000u128; // $0.12 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#253 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#253 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0254() { - // S=100.0, K=90.0, T=0.25, r=0.0 - // v0=0.09, kappa=1.0, theta=0.04, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 1000000000000u128, 40000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 11972469301249u128; - let exp_put = 1972469301249u128; - let tol = 400000000000u128; // $0.40 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#254 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#254 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0255() { - // S=100.0, K=100.0, T=1.0, r=0.0 - // v0=0.09, kappa=1.0, theta=0.04, xi=0.3, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 1000000000000u128, 40000000000u128, 300000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 10121692420855u128; - let exp_put = 10121692420855u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#255 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#255 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0256() { - // S=100.0, K=110.0, T=0.1, r=0.0 - // v0=0.09, kappa=1.0, theta=0.04, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 1000000000000u128, 40000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 536342938864u128; - let exp_put = 10536342938864u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#256 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#256 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0257() { - // S=100.0, K=120.0, T=0.5, r=0.0 - // v0=0.09, kappa=1.0, theta=0.04, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 1000000000000u128, 40000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 105522611441u128; - let exp_put = 20105522611441u128; - let tol = 800000000000u128; // $0.80 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#257 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#257 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0258() { - // S=100.0, K=85.0, T=0.1, r=0.0 - // v0=0.09, kappa=1.0, theta=0.09, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 1000000000000u128, 90000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 15179593442609u128; - let exp_put = 179593442609u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#258 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#258 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0259() { - // S=100.0, K=100.0, T=0.5, r=0.0 - // v0=0.09, kappa=1.0, theta=0.09, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 1000000000000u128, 90000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 8421755462272u128; - let exp_put = 8421755462272u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#259 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#259 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0260() { - // S=100.0, K=110.0, T=2.0, r=0.0 - // v0=0.09, kappa=1.0, theta=0.09, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 1000000000000u128, 90000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 12606135761676u128; - let exp_put = 22606135761676u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#260 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#260 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0261() { - // S=100.0, K=80.0, T=0.5, r=0.0 - // v0=0.09, kappa=1.0, theta=0.09, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 1000000000000u128, 90000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 21594514434046u128; - let exp_put = 1594514434046u128; - let tol = 125000000000u128; // $0.12 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#261 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#261 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0262() { - // S=100.0, K=90.0, T=2.0, r=0.0 - // v0=0.09, kappa=1.0, theta=0.09, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 1000000000000u128, 90000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 20620978836744u128; - let exp_put = 10620978836744u128; - let tol = 283333333333u128; // $0.28 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#262 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#262 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0263() { - // S=100.0, K=100.0, T=0.25, r=0.0 - // v0=0.09, kappa=1.0, theta=0.09, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 1000000000000u128, 90000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 5551055362645u128; - let exp_put = 5551055362645u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#263 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#263 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0264() { - // S=100.0, K=110.0, T=1.0, r=0.0 - // v0=0.09, kappa=1.0, theta=0.16, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 1000000000000u128, 160000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 9489037804809u128; - let exp_put = 19489037804809u128; - let tol = 460000000000u128; // $0.46 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#264 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#264 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0265() { - // S=100.0, K=80.0, T=0.25, r=0.0 - // v0=0.09, kappa=1.0, theta=0.16, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 1000000000000u128, 160000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 20616659453161u128; - let exp_put = 616659453161u128; - let tol = 460000000000u128; // $0.46 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#265 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#265 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0266() { - // S=100.0, K=95.0, T=1.0, r=0.0 - // v0=0.09, kappa=1.0, theta=0.16, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 1000000000000u128, 160000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 15581518623246u128; - let exp_put = 10581518623246u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#266 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#266 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0267() { - // S=100.0, K=105.0, T=0.1, r=0.0 - // v0=0.09, kappa=1.0, theta=0.16, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 1000000000000u128, 160000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 1666758967794u128; - let exp_put = 6666758967794u128; - let tol = 450000000000u128; // $0.45 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#267 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#267 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0268() { - // S=100.0, K=115.0, T=0.5, r=0.0 - // v0=0.09, kappa=1.0, theta=0.16, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 1000000000000u128, 160000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 3898967665072u128; - let exp_put = 18898967665072u128; - let tol = 289130434783u128; // $0.29 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#268 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#268 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0269() { - // S=100.0, K=80.0, T=0.1, r=0.0 - // v0=0.09, kappa=1.0, theta=0.16, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 1000000000000u128, 160000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 20052083120541u128; - let exp_put = 52083120541u128; - let tol = 625000000000u128; // $0.62 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#269 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#269 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0270() { - // S=100.0, K=90.0, T=0.5, r=0.0 - // v0=0.09, kappa=2.0, theta=0.01, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 2000000000000u128, 10000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 12791779265727u128; - let exp_put = 2791779265727u128; - let tol = 640000000000u128; // $0.64 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#270 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#270 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0271() { - // S=100.0, K=100.0, T=2.0, r=0.0 - // v0=0.09, kappa=2.0, theta=0.01, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 2000000000000u128, 10000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 9408887841441u128; - let exp_put = 9408887841441u128; - let tol = 80000000000u128; // $0.08 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#271 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#271 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0272() { - // S=100.0, K=110.0, T=0.25, r=0.0 - // v0=0.09, kappa=2.0, theta=0.01, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 2000000000000u128, 10000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 1752280036372u128; - let exp_put = 11752280036372u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#272 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#272 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0273() { - // S=100.0, K=120.0, T=1.0, r=0.0 - // v0=0.09, kappa=2.0, theta=0.01, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 2000000000000u128, 10000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 854582779852u128; - let exp_put = 20854582779852u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#273 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#273 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0274() { - // S=100.0, K=85.0, T=0.25, r=0.0 - // v0=0.09, kappa=2.0, theta=0.01, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 2000000000000u128, 10000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 16190818041198u128; - let exp_put = 1190818041198u128; - let tol = 602941176471u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#274 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#274 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0275() { - // S=100.0, K=95.0, T=1.0, r=0.0 - // v0=0.09, kappa=2.0, theta=0.04, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 2000000000000u128, 40000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 12352122787586u128; - let exp_put = 7352122787586u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#275 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#275 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0276() { - // S=100.0, K=105.0, T=0.1, r=0.0 - // v0=0.09, kappa=2.0, theta=0.04, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 2000000000000u128, 40000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 1675884047078u128; - let exp_put = 6675884047078u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#276 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#276 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0277() { - // S=100.0, K=120.0, T=0.5, r=0.0 - // v0=0.09, kappa=2.0, theta=0.04, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 2000000000000u128, 40000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 1837081011619u128; - let exp_put = 21837081011619u128; - let tol = 100000000000u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#277 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#277 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0278() { - // S=100.0, K=85.0, T=0.1, r=0.0 - // v0=0.09, kappa=2.0, theta=0.04, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 2000000000000u128, 40000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 15135418300611u128; - let exp_put = 135418300611u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#278 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#278 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0279() { - // S=100.0, K=95.0, T=0.5, r=0.0 - // v0=0.09, kappa=2.0, theta=0.04, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 2000000000000u128, 40000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 9954465086124u128; - let exp_put = 4954465086124u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#279 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#279 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0280() { - // S=100.0, K=105.0, T=2.0, r=0.0 - // v0=0.09, kappa=2.0, theta=0.04, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 2000000000000u128, 40000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 8695499475025u128; - let exp_put = 13695499475025u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#280 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#280 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0281() { - // S=100.0, K=115.0, T=0.25, r=0.0 - // v0=0.09, kappa=2.0, theta=0.09, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 2000000000000u128, 90000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 1439913648866u128; - let exp_put = 16439913648866u128; - let tol = 400000000000u128; // $0.40 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#281 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#281 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0282() { - // S=100.0, K=85.0, T=2.0, r=0.0 - // v0=0.09, kappa=2.0, theta=0.09, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 2000000000000u128, 90000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 24215039987487u128; - let exp_put = 9215039987487u128; - let tol = 102941176471u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#282 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#282 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0283() { - // S=100.0, K=95.0, T=0.25, r=0.0 - // v0=0.09, kappa=2.0, theta=0.09, xi=0.3, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 2000000000000u128, 90000000000u128, 300000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 8730034588369u128; - let exp_put = 3730034588369u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#283 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#283 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0284() { - // S=100.0, K=105.0, T=1.0, r=0.0 - // v0=0.09, kappa=2.0, theta=0.09, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 2000000000000u128, 90000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 8628403902371u128; - let exp_put = 13628403902371u128; - let tol = 450000000000u128; // $0.45 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#284 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#284 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0285() { - // S=100.0, K=120.0, T=0.1, r=0.0 - // v0=0.09, kappa=2.0, theta=0.09, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 2000000000000u128, 90000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 490738126u128; - let exp_put = 20000490738126u128; - let tol = 800000000000u128; // $0.80 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#285 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#285 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0286() { - // S=100.0, K=85.0, T=1.0, r=0.0 - // v0=0.09, kappa=2.0, theta=0.09, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 2000000000000u128, 90000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 19655181163146u128; - let exp_put = 4655181163146u128; - let tol = 602941176471u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#286 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#286 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0287() { - // S=100.0, K=95.0, T=0.1, r=0.0 - // v0=0.09, kappa=2.0, theta=0.16, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 2000000000000u128, 160000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 6803892172681u128; - let exp_put = 1803892172681u128; - let tol = 610000000000u128; // $0.61 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#287 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#287 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0288() { - // S=100.0, K=110.0, T=0.5, r=0.0 - // v0=0.09, kappa=2.0, theta=0.16, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 2000000000000u128, 160000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 5682163015486u128; - let exp_put = 15682163015486u128; - let tol = 70000000000u128; // $0.07 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#288 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#288 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0289() { - // S=100.0, K=120.0, T=2.0, r=0.0 - // v0=0.09, kappa=2.0, theta=0.16, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 2000000000000u128, 160000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 13687241467521u128; - let exp_put = 33687241467521u128; - let tol = 100000000000u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#289 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#289 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0290() { - // S=100.0, K=85.0, T=0.5, r=0.0 - // v0=0.09, kappa=2.0, theta=0.16, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 2000000000000u128, 160000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 18510404441865u128; - let exp_put = 3510404441865u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#290 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#290 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0291() { - // S=100.0, K=95.0, T=2.0, r=0.0 - // v0=0.09, kappa=2.0, theta=0.16, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 2000000000000u128, 160000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 21621712353191u128; - let exp_put = 16621712353191u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#291 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#291 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0292() { - // S=100.0, K=105.0, T=0.25, r=0.0 - // v0=0.09, kappa=3.0, theta=0.01, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 3000000000000u128, 10000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 3059510806000u128; - let exp_put = 8059510806000u128; - let tol = 790000000000u128; // $0.79 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#292 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#292 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0293() { - // S=100.0, K=115.0, T=1.0, r=0.0 - // v0=0.09, kappa=3.0, theta=0.01, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 3000000000000u128, 10000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 1930746098199u128; - let exp_put = 16930746098199u128; - let tol = 289130434783u128; // $0.29 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#293 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#293 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0294() { - // S=100.0, K=80.0, T=0.25, r=0.0 - // v0=0.09, kappa=3.0, theta=0.01, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 3000000000000u128, 10000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 20389236694888u128; - let exp_put = 389236694888u128; - let tol = 325000000000u128; // $0.33 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#294 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#294 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0295() { - // S=100.0, K=90.0, T=1.0, r=0.0 - // v0=0.09, kappa=3.0, theta=0.01, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 3000000000000u128, 10000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 13106948486329u128; - let exp_put = 3106948486329u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#295 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#295 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0296() { - // S=100.0, K=100.0, T=0.1, r=0.0 - // v0=0.09, kappa=3.0, theta=0.01, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 3000000000000u128, 10000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 3486775262935u128; - let exp_put = 3486775262935u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#296 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#296 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0297() { - // S=100.0, K=110.0, T=0.5, r=0.0 - // v0=0.09, kappa=3.0, theta=0.01, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 3000000000000u128, 10000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 2101145561757u128; - let exp_put = 12101145561757u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#297 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#297 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0298() { - // S=100.0, K=120.0, T=2.0, r=0.0 - // v0=0.09, kappa=3.0, theta=0.04, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 3000000000000u128, 40000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 5627921006745u128; - let exp_put = 25627921006745u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#298 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#298 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0299() { - // S=100.0, K=90.0, T=0.5, r=0.0 - // v0=0.09, kappa=3.0, theta=0.04, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 3000000000000u128, 40000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 13092396883726u128; - let exp_put = 3092396883726u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#299 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#299 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0300() { - // S=100.0, K=100.0, T=2.0, r=0.0 - // v0=0.09, kappa=3.0, theta=0.04, xi=0.3, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 3000000000000u128, 40000000000u128, 300000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 12007829681873u128; - let exp_put = 12007829681873u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#300 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#300 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0301() { - // S=100.0, K=110.0, T=0.25, r=0.0 - // v0=0.09, kappa=3.0, theta=0.04, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 3000000000000u128, 40000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 1516974801003u128; - let exp_put = 11516974801003u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#301 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#301 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0302() { - // S=100.0, K=120.0, T=1.0, r=0.0 - // v0=0.09, kappa=3.0, theta=0.04, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 3000000000000u128, 40000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 657611714374u128; - let exp_put = 20657611714374u128; - let tol = 800000000000u128; // $0.80 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#302 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#302 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0303() { - // S=100.0, K=85.0, T=0.25, r=0.0 - // v0=0.09, kappa=3.0, theta=0.09, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 3000000000000u128, 90000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 16062913809297u128; - let exp_put = 1062913809297u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#303 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#303 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0304() { - // S=100.0, K=100.0, T=1.0, r=0.0 - // v0=0.09, kappa=3.0, theta=0.09, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 3000000000000u128, 90000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 11913617178364u128; - let exp_put = 11913617178364u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#304 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#304 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0305() { - // S=100.0, K=110.0, T=0.1, r=0.0 - // v0=0.09, kappa=3.0, theta=0.09, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 3000000000000u128, 90000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 804758113482u128; - let exp_put = 10804758113482u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#305 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#305 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0306() { - // S=100.0, K=120.0, T=0.5, r=0.0 - // v0=0.09, kappa=3.0, theta=0.09, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 3000000000000u128, 90000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 2272319182776u128; - let exp_put = 22272319182776u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#306 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#306 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0307() { - // S=100.0, K=85.0, T=0.1, r=0.0 - // v0=0.09, kappa=3.0, theta=0.09, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 3000000000000u128, 90000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 15207042579543u128; - let exp_put = 207042579543u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#307 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#307 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0308() { - // S=100.0, K=95.0, T=0.5, r=0.0 - // v0=0.09, kappa=3.0, theta=0.09, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 3000000000000u128, 90000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 10714513365136u128; - let exp_put = 5714513365136u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#308 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#308 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0309() { - // S=100.0, K=105.0, T=2.0, r=0.0 - // v0=0.09, kappa=3.0, theta=0.16, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 3000000000000u128, 160000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 19442688398693u128; - let exp_put = 24442688398693u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#309 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#309 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0310() { - // S=100.0, K=115.0, T=0.25, r=0.0 - // v0=0.09, kappa=3.0, theta=0.16, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 3000000000000u128, 160000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 1795921316274u128; - let exp_put = 16795921316274u128; - let tol = 760000000000u128; // $0.76 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#310 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#310 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0311() { - // S=100.0, K=80.0, T=2.0, r=0.0 - // v0=0.09, kappa=3.0, theta=0.16, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 3000000000000u128, 160000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 30941759062393u128; - let exp_put = 10941759062393u128; - let tol = 325000000000u128; // $0.33 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#311 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#311 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0312() { - // S=100.0, K=90.0, T=0.25, r=0.0 - // v0=0.09, kappa=3.0, theta=0.16, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 3000000000000u128, 160000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 12786280175535u128; - let exp_put = 2786280175535u128; - let tol = 483333333333u128; // $0.48 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#312 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#312 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0313() { - // S=100.0, K=100.0, T=1.0, r=0.0 - // v0=0.09, kappa=3.0, theta=0.16, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 3000000000000u128, 160000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 14544542930660u128; - let exp_put = 14544542930660u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#313 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#313 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0314() { - // S=100.0, K=110.0, T=0.1, r=0.0 - // v0=0.09, kappa=3.0, theta=0.16, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 3000000000000u128, 160000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 926360342562u128; - let exp_put = 10926360342562u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#314 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#314 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0315() { - // S=100.0, K=120.0, T=0.5, r=0.0 - // v0=0.09, kappa=5.0, theta=0.01, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 5000000000000u128, 10000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 643664532334u128; - let exp_put = 20643664532334u128; - let tol = 1090000000000u128; // $1.09 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#315 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#315 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0316() { - // S=100.0, K=90.0, T=0.1, r=0.0 - // v0=0.09, kappa=5.0, theta=0.01, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 5000000000000u128, 10000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 10444626343870u128; - let exp_put = 444626343870u128; - let tol = 1090000000000u128; // $1.09 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#316 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#316 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0317() { - // S=100.0, K=100.0, T=0.5, r=0.0 - // v0=0.09, kappa=5.0, theta=0.01, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 5000000000000u128, 10000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 5478679773196u128; - let exp_put = 5478679773196u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#317 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#317 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0318() { - // S=100.0, K=110.0, T=2.0, r=0.0 - // v0=0.09, kappa=5.0, theta=0.01, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 5000000000000u128, 10000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 3000145635923u128; - let exp_put = 13000145635923u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#318 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#318 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0319() { - // S=100.0, K=120.0, T=0.25, r=0.0 - // v0=0.09, kappa=5.0, theta=0.01, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 5000000000000u128, 10000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 44309020906u128; - let exp_put = 20044309020906u128; - let tol = 600000000000u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#319 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#319 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0320() { - // S=100.0, K=85.0, T=2.0, r=0.0 - // v0=0.09, kappa=5.0, theta=0.04, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 5000000000000u128, 40000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 20185031582566u128; - let exp_put = 5185031582566u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#320 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#320 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0321() { - // S=100.0, K=95.0, T=0.25, r=0.0 - // v0=0.09, kappa=5.0, theta=0.04, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 5000000000000u128, 40000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 8048565765560u128; - let exp_put = 3048565765560u128; - let tol = 1000000000000u128; // $1.00 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#321 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#321 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0322() { - // S=100.0, K=105.0, T=1.0, r=0.0 - // v0=0.09, kappa=5.0, theta=0.04, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 5000000000000u128, 40000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 6808692217506u128; - let exp_put = 11808692217506u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#322 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#322 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0323() { - // S=100.0, K=115.0, T=0.1, r=0.0 - // v0=0.09, kappa=5.0, theta=0.04, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 5000000000000u128, 40000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 237392356782u128; - let exp_put = 15237392356782u128; - let tol = 1000000000000u128; // $1.00 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#323 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#323 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0324() { - // S=100.0, K=80.0, T=1.0, r=0.0 - // v0=0.09, kappa=5.0, theta=0.04, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 5000000000000u128, 40000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 21863158652403u128; - let exp_put = 1863158652403u128; - let tol = 325000000000u128; // $0.33 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#324 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#324 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0325() { - // S=100.0, K=90.0, T=0.1, r=0.0 - // v0=0.09, kappa=5.0, theta=0.04, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 5000000000000u128, 40000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 10594366318603u128; - let exp_put = 594366318603u128; - let tol = 583333333333u128; // $0.58 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#325 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#325 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0326() { - // S=100.0, K=100.0, T=0.5, r=0.0 - // v0=0.09, kappa=5.0, theta=0.09, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 5000000000000u128, 90000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 8417415448363u128; - let exp_put = 8417415448363u128; - let tol = 850000000000u128; // $0.85 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#326 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#326 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0327() { - // S=100.0, K=115.0, T=2.0, r=0.0 - // v0=0.09, kappa=5.0, theta=0.09, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 5000000000000u128, 90000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 11223755393053u128; - let exp_put = 26223755393053u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#327 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#327 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0328() { - // S=100.0, K=80.0, T=0.5, r=0.0 - // v0=0.09, kappa=5.0, theta=0.09, xi=0.3, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 5000000000000u128, 90000000000u128, 300000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 21654554696428u128; - let exp_put = 1654554696428u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#328 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#328 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0329() { - // S=100.0, K=90.0, T=2.0, r=0.0 - // v0=0.09, kappa=5.0, theta=0.09, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 2000000000000u128, - 90000000000u128, 5000000000000u128, 90000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 21345455454080u128; - let exp_put = 11345455454080u128; - let tol = 483333333333u128; // $0.48 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#329 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#329 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0330() { - // S=100.0, K=100.0, T=0.25, r=0.0 - // v0=0.09, kappa=5.0, theta=0.09, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 5000000000000u128, 90000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 5668000454222u128; - let exp_put = 5668000454222u128; - let tol = 750000000000u128; // $0.75 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#330 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#330 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0331() { - // S=100.0, K=110.0, T=1.0, r=0.0 - // v0=0.09, kappa=5.0, theta=0.09, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 5000000000000u128, 90000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 7895817221501u128; - let exp_put = 17895817221501u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#331 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#331 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0332() { - // S=100.0, K=80.0, T=0.25, r=0.0 - // v0=0.09, kappa=5.0, theta=0.16, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 250000000000u128, - 90000000000u128, 5000000000000u128, 160000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 20717960109842u128; - let exp_put = 717960109842u128; - let tol = 1060000000000u128; // $1.06 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#332 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#332 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0333() { - // S=100.0, K=95.0, T=1.0, r=0.0 - // v0=0.09, kappa=5.0, theta=0.16, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 1000000000000u128, - 90000000000u128, 5000000000000u128, 160000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 17383918769463u128; - let exp_put = 12383918769463u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#333 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#333 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0334() { - // S=100.0, K=105.0, T=0.1, r=0.0 - // v0=0.09, kappa=5.0, theta=0.16, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 5000000000000u128, 160000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 2081433901951u128; - let exp_put = 7081433901951u128; - let tol = 1060000000000u128; // $1.06 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#334 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#334 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0335() { - // S=100.0, K=115.0, T=0.5, r=0.0 - // v0=0.09, kappa=5.0, theta=0.16, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 500000000000u128, - 90000000000u128, 5000000000000u128, 160000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 4695387344320u128; - let exp_put = 19695387344320u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#335 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#335 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0336() { - // S=100.0, K=80.0, T=0.1, r=0.0 - // v0=0.09, kappa=5.0, theta=0.16, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 100000000000u128, - 90000000000u128, 5000000000000u128, 160000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 20122615297806u128; - let exp_put = 122615297806u128; - let tol = 625000000000u128; // $0.62 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#336 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#336 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0337() { - // S=100.0, K=90.0, T=0.5, r=0.0 - // v0=0.16, kappa=0.5, theta=0.01, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 500000000000u128, 10000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 15917524346743u128; - let exp_put = 5917524346743u128; - let tol = 625000000000u128; // $0.62 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#337 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#337 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0338() { - // S=100.0, K=100.0, T=2.0, r=0.0 - // v0=0.16, kappa=0.5, theta=0.01, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 500000000000u128, 10000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 17135116367727u128; - let exp_put = 17135116367727u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#338 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#338 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0339() { - // S=100.0, K=110.0, T=0.25, r=0.0 - // v0=0.16, kappa=0.5, theta=0.01, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 500000000000u128, 10000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 3668860486732u128; - let exp_put = 13668860486732u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#339 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#339 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0340() { - // S=100.0, K=120.0, T=1.0, r=0.0 - // v0=0.16, kappa=0.5, theta=0.01, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 500000000000u128, 10000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 7352534514818u128; - let exp_put = 27352534514818u128; - let tol = 100000000000u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#340 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#340 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0341() { - // S=100.0, K=85.0, T=0.25, r=0.0 - // v0=0.16, kappa=0.5, theta=0.01, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 500000000000u128, 10000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 16985120886159u128; - let exp_put = 1985120886159u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#341 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#341 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0342() { - // S=100.0, K=95.0, T=1.0, r=0.0 - // v0=0.16, kappa=0.5, theta=0.01, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 500000000000u128, 10000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 14652431061734u128; - let exp_put = 9652431061734u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#342 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#342 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0343() { - // S=100.0, K=105.0, T=0.1, r=0.0 - // v0=0.16, kappa=0.5, theta=0.04, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 500000000000u128, 40000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 2958285662886u128; - let exp_put = 7958285662886u128; - let tol = 535000000000u128; // $0.53 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#343 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#343 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0344() { - // S=100.0, K=115.0, T=0.5, r=0.0 - // v0=0.16, kappa=0.5, theta=0.04, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 500000000000u128, 40000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 5216477271098u128; - let exp_put = 20216477271098u128; - let tol = 89130434783u128; // $0.09 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#344 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#344 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0345() { - // S=100.0, K=80.0, T=0.1, r=0.0 - // v0=0.16, kappa=0.5, theta=0.04, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 500000000000u128, 40000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 20210489076124u128; - let exp_put = 210489076124u128; - let tol = 535000000000u128; // $0.53 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#345 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#345 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0346() { - // S=100.0, K=90.0, T=0.5, r=0.0 - // v0=0.16, kappa=0.5, theta=0.04, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 500000000000u128, 40000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 16082503501803u128; - let exp_put = 6082503501803u128; - let tol = 283333333333u128; // $0.28 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#346 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#346 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0347() { - // S=100.0, K=100.0, T=2.0, r=0.0 - // v0=0.16, kappa=0.5, theta=0.04, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 500000000000u128, 40000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 12828955203833u128; - let exp_put = 12828955203833u128; - let tol = 750000000000u128; // $0.75 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#347 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#347 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0348() { - // S=100.0, K=115.0, T=0.25, r=0.0 - // v0=0.16, kappa=0.5, theta=0.09, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 500000000000u128, 90000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 2815478823903u128; - let exp_put = 17815478823903u128; - let tol = 385000000000u128; // $0.39 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#348 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#348 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0349() { - // S=100.0, K=80.0, T=2.0, r=0.0 - // v0=0.16, kappa=0.5, theta=0.09, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 500000000000u128, 90000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 29985274082798u128; - let exp_put = 9985274082798u128; - let tol = 125000000000u128; // $0.12 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#349 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#349 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0350() { - // S=100.0, K=90.0, T=0.25, r=0.0 - // v0=0.16, kappa=0.5, theta=0.09, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 500000000000u128, 90000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 13482668718421u128; - let exp_put = 3482668718421u128; - let tol = 385000000000u128; // $0.39 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#350 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#350 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0351() { - // S=100.0, K=100.0, T=1.0, r=0.0 - // v0=0.16, kappa=0.5, theta=0.09, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 500000000000u128, 90000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 14675406882212u128; - let exp_put = 14675406882212u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#351 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#351 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0352() { - // S=100.0, K=110.0, T=0.1, r=0.0 - // v0=0.16, kappa=0.5, theta=0.09, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 500000000000u128, 90000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 1573546901652u128; - let exp_put = 11573546901652u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#352 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#352 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0353() { - // S=100.0, K=120.0, T=0.5, r=0.0 - // v0=0.16, kappa=0.5, theta=0.09, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 500000000000u128, 90000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 2945425727600u128; - let exp_put = 22945425727600u128; - let tol = 600000000000u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#353 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#353 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0354() { - // S=100.0, K=85.0, T=0.1, r=0.0 - // v0=0.16, kappa=0.5, theta=0.16, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 500000000000u128, 160000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 15557449544643u128; - let exp_put = 557449544643u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#354 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#354 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0355() { - // S=100.0, K=95.0, T=0.5, r=0.0 - // v0=0.16, kappa=0.5, theta=0.16, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 500000000000u128, 160000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 13612026102483u128; - let exp_put = 8612026102483u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#355 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#355 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0356() { - // S=100.0, K=105.0, T=2.0, r=0.0 - // v0=0.16, kappa=0.5, theta=0.16, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 500000000000u128, 160000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 18466799734656u128; - let exp_put = 23466799734656u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#356 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#356 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0357() { - // S=100.0, K=115.0, T=0.25, r=0.0 - // v0=0.16, kappa=0.5, theta=0.16, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 500000000000u128, 160000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 2209295852399u128; - let exp_put = 17209295852399u128; - let tol = 489130434783u128; // $0.49 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#357 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#357 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0358() { - // S=100.0, K=80.0, T=2.0, r=0.0 - // v0=0.16, kappa=0.5, theta=0.16, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 500000000000u128, 160000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 30610911047230u128; - let exp_put = 10610911047230u128; - let tol = 325000000000u128; // $0.33 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#358 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#358 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0359() { - // S=100.0, K=90.0, T=0.25, r=0.0 - // v0=0.16, kappa=0.5, theta=0.16, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 500000000000u128, 160000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 13416279806562u128; - let exp_put = 3416279806562u128; - let tol = 583333333333u128; // $0.58 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#359 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#359 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0360() { - // S=100.0, K=100.0, T=1.0, r=0.0 - // v0=0.16, kappa=1.0, theta=0.01, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 1000000000000u128, 10000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 12789512065334u128; - let exp_put = 12789512065334u128; - let tol = 700000000000u128; // $0.70 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#360 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#360 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0361() { - // S=100.0, K=110.0, T=0.1, r=0.0 - // v0=0.16, kappa=1.0, theta=0.01, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 1000000000000u128, 10000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 1572431138366u128; - let exp_put = 11572431138366u128; - let tol = 700000000000u128; // $0.70 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#361 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#361 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0362() { - // S=100.0, K=120.0, T=0.5, r=0.0 - // v0=0.16, kappa=1.0, theta=0.01, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 1000000000000u128, 10000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 3249102369171u128; - let exp_put = 23249102369171u128; - let tol = 100000000000u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#362 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#362 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0363() { - // S=100.0, K=85.0, T=0.1, r=0.0 - // v0=0.16, kappa=1.0, theta=0.01, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 1000000000000u128, 10000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 15606858426463u128; - let exp_put = 606858426463u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#363 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#363 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0364() { - // S=100.0, K=95.0, T=0.5, r=0.0 - // v0=0.16, kappa=1.0, theta=0.01, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 1000000000000u128, 10000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 11991024057293u128; - let exp_put = 6991024057293u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#364 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#364 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0365() { - // S=100.0, K=105.0, T=2.0, r=0.0 - // v0=0.16, kappa=1.0, theta=0.04, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 1000000000000u128, 40000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 14575044767493u128; - let exp_put = 19575044767493u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#365 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#365 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0366() { - // S=100.0, K=115.0, T=0.25, r=0.0 - // v0=0.16, kappa=1.0, theta=0.04, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 1000000000000u128, 40000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 2456960464423u128; - let exp_put = 17456960464423u128; - let tol = 610000000000u128; // $0.61 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#366 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#366 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0367() { - // S=100.0, K=80.0, T=2.0, r=0.0 - // v0=0.16, kappa=1.0, theta=0.04, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 1000000000000u128, 40000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 27137611540144u128; - let exp_put = 7137611540144u128; - let tol = 125000000000u128; // $0.12 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#367 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#367 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0368() { - // S=100.0, K=90.0, T=0.25, r=0.0 - // v0=0.16, kappa=1.0, theta=0.04, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 1000000000000u128, 40000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 13269693682429u128; - let exp_put = 3269693682429u128; - let tol = 283333333333u128; // $0.28 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#368 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#368 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0369() { - // S=100.0, K=100.0, T=1.0, r=0.0 - // v0=0.16, kappa=1.0, theta=0.04, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 1000000000000u128, 40000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 12653904763652u128; - let exp_put = 12653904763652u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#369 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#369 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0370() { - // S=100.0, K=110.0, T=0.1, r=0.0 - // v0=0.16, kappa=1.0, theta=0.04, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 1000000000000u128, 40000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 1453540711057u128; - let exp_put = 11453540711057u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#370 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#370 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0371() { - // S=100.0, K=120.0, T=0.5, r=0.0 - // v0=0.16, kappa=1.0, theta=0.09, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 1000000000000u128, 90000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 4188914270777u128; - let exp_put = 24188914270777u128; - let tol = 460000000000u128; // $0.46 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#371 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#371 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0372() { - // S=100.0, K=85.0, T=0.1, r=0.0 - // v0=0.16, kappa=1.0, theta=0.09, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 1000000000000u128, 90000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 15559846862115u128; - let exp_put = 559846862115u128; - let tol = 460000000000u128; // $0.46 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#372 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#372 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0373() { - // S=100.0, K=95.0, T=0.5, r=0.0 - // v0=0.16, kappa=1.0, theta=0.09, xi=0.3, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 1000000000000u128, 90000000000u128, 300000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 13080564299984u128; - let exp_put = 8080564299984u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#373 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#373 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0374() { - // S=100.0, K=105.0, T=2.0, r=0.0 - // v0=0.16, kappa=1.0, theta=0.09, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 1000000000000u128, 90000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 14552434925316u128; - let exp_put = 19552434925316u128; - let tol = 450000000000u128; // $0.45 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#374 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#374 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0375() { - // S=100.0, K=115.0, T=0.25, r=0.0 - // v0=0.16, kappa=1.0, theta=0.09, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 1000000000000u128, 90000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 1424167034493u128; - let exp_put = 16424167034493u128; - let tol = 789130434783u128; // $0.79 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#375 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#375 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0376() { - // S=100.0, K=80.0, T=2.0, r=0.0 - // v0=0.16, kappa=1.0, theta=0.09, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 1000000000000u128, 90000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 27932441650788u128; - let exp_put = 7932441650788u128; - let tol = 625000000000u128; // $0.62 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#376 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#376 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0377() { - // S=100.0, K=90.0, T=0.25, r=0.0 - // v0=0.16, kappa=1.0, theta=0.16, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 1000000000000u128, 160000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 13577536812077u128; - let exp_put = 3577536812077u128; - let tol = 283333333333u128; // $0.28 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#377 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#377 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0378() { - // S=100.0, K=100.0, T=1.0, r=0.0 - // v0=0.16, kappa=1.0, theta=0.16, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 1000000000000u128, 160000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 15681697676496u128; - let exp_put = 15681697676496u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#378 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#378 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0379() { - // S=100.0, K=110.0, T=0.1, r=0.0 - // v0=0.16, kappa=1.0, theta=0.16, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 1000000000000u128, 160000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 1640861214558u128; - let exp_put = 11640861214558u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#379 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#379 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0380() { - // S=100.0, K=120.0, T=0.5, r=0.0 - // v0=0.16, kappa=1.0, theta=0.16, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 1000000000000u128, 160000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 3926019836137u128; - let exp_put = 23926019836137u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#380 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#380 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0381() { - // S=100.0, K=85.0, T=0.1, r=0.0 - // v0=0.16, kappa=1.0, theta=0.16, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 1000000000000u128, 160000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 15718028854316u128; - let exp_put = 718028854316u128; - let tol = 602941176471u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#381 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#381 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0382() { - // S=100.0, K=95.0, T=0.5, r=0.0 - // v0=0.16, kappa=2.0, theta=0.01, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 2000000000000u128, 10000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 11618916484509u128; - let exp_put = 6618916484509u128; - let tol = 850000000000u128; // $0.85 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#382 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#382 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0383() { - // S=100.0, K=105.0, T=2.0, r=0.0 - // v0=0.16, kappa=2.0, theta=0.01, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 2000000000000u128, 10000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 9433711016946u128; - let exp_put = 14433711016946u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#383 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#383 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0384() { - // S=100.0, K=115.0, T=0.25, r=0.0 - // v0=0.16, kappa=2.0, theta=0.01, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 2000000000000u128, 10000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 1910605473832u128; - let exp_put = 16910605473832u128; - let tol = 289130434783u128; // $0.29 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#384 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#384 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0385() { - // S=100.0, K=80.0, T=2.0, r=0.0 - // v0=0.16, kappa=2.0, theta=0.01, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 2000000000000u128, 10000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 23606751857644u128; - let exp_put = 3606751857644u128; - let tol = 150000000000u128; // $0.15 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#385 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#385 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0386() { - // S=100.0, K=90.0, T=0.25, r=0.0 - // v0=0.16, kappa=2.0, theta=0.01, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 2000000000000u128, 10000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 12846880499352u128; - let exp_put = 2846880499352u128; - let tol = 283333333333u128; // $0.28 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#386 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#386 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0387() { - // S=100.0, K=100.0, T=1.0, r=0.0 - // v0=0.16, kappa=2.0, theta=0.01, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 2000000000000u128, 10000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 9543780405216u128; - let exp_put = 9543780405216u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#387 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#387 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0388() { - // S=100.0, K=110.0, T=0.1, r=0.0 - // v0=0.16, kappa=2.0, theta=0.04, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 2000000000000u128, 40000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 1544118699716u128; - let exp_put = 11544118699716u128; - let tol = 760000000000u128; // $0.76 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#388 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#388 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0389() { - // S=100.0, K=80.0, T=1.0, r=0.0 - // v0=0.16, kappa=2.0, theta=0.04, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 2000000000000u128, 40000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 23824965620189u128; - let exp_put = 3824965620189u128; - let tol = 125000000000u128; // $0.12 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#389 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#389 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0390() { - // S=100.0, K=90.0, T=0.1, r=0.0 - // v0=0.16, kappa=2.0, theta=0.04, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 2000000000000u128, 40000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 11289759528583u128; - let exp_put = 1289759528583u128; - let tol = 760000000000u128; // $0.76 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#390 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#390 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0391() { - // S=100.0, K=100.0, T=0.5, r=0.0 - // v0=0.16, kappa=2.0, theta=0.04, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 2000000000000u128, 40000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 9158671428307u128; - let exp_put = 9158671428307u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#391 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#391 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0392() { - // S=100.0, K=110.0, T=2.0, r=0.0 - // v0=0.16, kappa=2.0, theta=0.04, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 2000000000000u128, 40000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 7166862064717u128; - let exp_put = 17166862064717u128; - let tol = 750000000000u128; // $0.75 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#392 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#392 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0393() { - // S=100.0, K=120.0, T=0.25, r=0.0 - // v0=0.16, kappa=2.0, theta=0.09, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 2000000000000u128, 90000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 1726492017566u128; - let exp_put = 21726492017566u128; - let tol = 610000000000u128; // $0.61 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#393 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#393 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0394() { - // S=100.0, K=85.0, T=2.0, r=0.0 - // v0=0.16, kappa=2.0, theta=0.09, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 2000000000000u128, 90000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 25431360729475u128; - let exp_put = 10431360729475u128; - let tol = 102941176471u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#394 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#394 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0395() { - // S=100.0, K=95.0, T=0.25, r=0.0 - // v0=0.16, kappa=2.0, theta=0.09, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 2000000000000u128, 90000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 10139754535331u128; - let exp_put = 5139754535331u128; - let tol = 610000000000u128; // $0.61 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#395 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#395 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0396() { - // S=100.0, K=105.0, T=1.0, r=0.0 - // v0=0.16, kappa=2.0, theta=0.09, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 2000000000000u128, 90000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 11446597343437u128; - let exp_put = 16446597343437u128; - let tol = 70000000000u128; // $0.07 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#396 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#396 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0397() { - // S=100.0, K=115.0, T=0.1, r=0.0 - // v0=0.16, kappa=2.0, theta=0.09, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 2000000000000u128, 90000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 752316771406u128; - let exp_put = 15752316771406u128; - let tol = 289130434783u128; // $0.29 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#397 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#397 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0398() { - // S=100.0, K=80.0, T=1.0, r=0.0 - // v0=0.16, kappa=2.0, theta=0.09, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 2000000000000u128, 90000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 25034994863481u128; - let exp_put = 5034994863481u128; - let tol = 625000000000u128; // $0.62 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#398 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#398 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0399() { - // S=100.0, K=90.0, T=0.1, r=0.0 - // v0=0.16, kappa=2.0, theta=0.16, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 2000000000000u128, 160000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 11359191412867u128; - let exp_put = 1359191412867u128; - let tol = 400000000000u128; // $0.40 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#399 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#399 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0400() { - // S=100.0, K=100.0, T=0.5, r=0.0 - // v0=0.16, kappa=2.0, theta=0.16, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 2000000000000u128, 160000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 11129592277718u128; - let exp_put = 11129592277718u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#400 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#400 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0401() { - // S=100.0, K=110.0, T=2.0, r=0.0 - // v0=0.16, kappa=2.0, theta=0.16, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 2000000000000u128, 160000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 17638328776117u128; - let exp_put = 27638328776117u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#401 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#401 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0402() { - // S=100.0, K=120.0, T=0.25, r=0.0 - // v0=0.16, kappa=2.0, theta=0.16, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 2000000000000u128, 160000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 1368024470103u128; - let exp_put = 21368024470103u128; - let tol = 500000000000u128; // $0.50 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#402 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#402 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0403() { - // S=100.0, K=85.0, T=2.0, r=0.0 - // v0=0.16, kappa=2.0, theta=0.16, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 2000000000000u128, 160000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 28641253024863u128; - let exp_put = 13641253024863u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#403 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#403 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0404() { - // S=100.0, K=95.0, T=0.25, r=0.0 - // v0=0.16, kappa=2.0, theta=0.16, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 2000000000000u128, 160000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 10312161886840u128; - let exp_put = 5312161886840u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#404 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#404 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0405() { - // S=100.0, K=105.0, T=1.0, r=0.0 - // v0=0.16, kappa=3.0, theta=0.01, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 3000000000000u128, 10000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 7415080359966u128; - let exp_put = 12415080359966u128; - let tol = 1000000000000u128; // $1.00 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#405 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#405 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0406() { - // S=100.0, K=115.0, T=0.1, r=0.0 - // v0=0.16, kappa=3.0, theta=0.01, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 3000000000000u128, 10000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 684237268233u128; - let exp_put = 15684237268233u128; - let tol = 1000000000000u128; // $1.00 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#406 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#406 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0407() { - // S=100.0, K=80.0, T=1.0, r=0.0 - // v0=0.16, kappa=3.0, theta=0.01, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 3000000000000u128, 10000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 22339467069189u128; - let exp_put = 2339467069189u128; - let tol = 225000000000u128; // $0.22 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#407 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#407 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0408() { - // S=100.0, K=90.0, T=0.1, r=0.0 - // v0=0.16, kappa=3.0, theta=0.01, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 3000000000000u128, 10000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 11236930999358u128; - let exp_put = 1236930999358u128; - let tol = 283333333333u128; // $0.28 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#408 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#408 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0409() { - // S=100.0, K=100.0, T=0.5, r=0.0 - // v0=0.16, kappa=3.0, theta=0.01, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 3000000000000u128, 10000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 7512319516909u128; - let exp_put = 7512319516909u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#409 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#409 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0410() { - // S=100.0, K=110.0, T=2.0, r=0.0 - // v0=0.16, kappa=3.0, theta=0.04, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 3000000000000u128, 40000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 9713709541516u128; - let exp_put = 19713709541516u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#410 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#410 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0411() { - // S=100.0, K=120.0, T=0.25, r=0.0 - // v0=0.16, kappa=3.0, theta=0.04, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 3000000000000u128, 40000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 1216177225581u128; - let exp_put = 21216177225581u128; - let tol = 910000000000u128; // $0.91 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#411 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#411 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0412() { - // S=100.0, K=85.0, T=2.0, r=0.0 - // v0=0.16, kappa=3.0, theta=0.04, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 3000000000000u128, 40000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 21538165643768u128; - let exp_put = 6538165643768u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#412 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#412 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0413() { - // S=100.0, K=95.0, T=0.25, r=0.0 - // v0=0.16, kappa=3.0, theta=0.04, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 3000000000000u128, 40000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 9601438162823u128; - let exp_put = 4601438162823u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#413 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#413 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0414() { - // S=100.0, K=105.0, T=1.0, r=0.0 - // v0=0.16, kappa=3.0, theta=0.04, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 3000000000000u128, 40000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 8530528175256u128; - let exp_put = 13530528175256u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#414 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#414 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0415() { - // S=100.0, K=115.0, T=0.1, r=0.0 - // v0=0.16, kappa=3.0, theta=0.04, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 3000000000000u128, 40000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 627618054460u128; - let exp_put = 15627618054460u128; - let tol = 589130434783u128; // $0.59 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#415 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#415 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0416() { - // S=100.0, K=80.0, T=1.0, r=0.0 - // v0=0.16, kappa=3.0, theta=0.09, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 3000000000000u128, 90000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 24572264570590u128; - let exp_put = 4572264570590u128; - let tol = 760000000000u128; // $0.76 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#416 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#416 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0417() { - // S=100.0, K=90.0, T=0.1, r=0.0 - // v0=0.16, kappa=3.0, theta=0.09, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 3000000000000u128, 90000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 11283258196997u128; - let exp_put = 1283258196997u128; - let tol = 760000000000u128; // $0.76 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#417 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#417 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0418() { - // S=100.0, K=100.0, T=0.5, r=0.0 - // v0=0.16, kappa=3.0, theta=0.09, xi=0.3, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 3000000000000u128, 90000000000u128, 300000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 9834445121644u128; - let exp_put = 9834445121644u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#418 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#418 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0419() { - // S=100.0, K=110.0, T=2.0, r=0.0 - // v0=0.16, kappa=3.0, theta=0.09, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 3000000000000u128, 90000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 12721572578210u128; - let exp_put = 22721572578210u128; - let tol = 450000000000u128; // $0.45 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#419 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#419 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0420() { - // S=100.0, K=120.0, T=0.25, r=0.0 - // v0=0.16, kappa=3.0, theta=0.09, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 3000000000000u128, 90000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 574147845297u128; - let exp_put = 20574147845297u128; - let tol = 800000000000u128; // $0.80 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#420 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#420 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0421() { - // S=100.0, K=85.0, T=2.0, r=0.0 - // v0=0.16, kappa=3.0, theta=0.09, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 3000000000000u128, 90000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 24625497249220u128; - let exp_put = 9625497249220u128; - let tol = 602941176471u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#421 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#421 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0422() { - // S=100.0, K=95.0, T=0.25, r=0.0 - // v0=0.16, kappa=3.0, theta=0.16, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 3000000000000u128, 160000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 10506564895139u128; - let exp_put = 5506564895139u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#422 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#422 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0423() { - // S=100.0, K=105.0, T=1.0, r=0.0 - // v0=0.16, kappa=3.0, theta=0.16, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 3000000000000u128, 160000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 13749169450215u128; - let exp_put = 18749169450215u128; - let tol = 180000000000u128; // $0.18 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#423 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#423 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0424() { - // S=100.0, K=115.0, T=0.1, r=0.0 - // v0=0.16, kappa=3.0, theta=0.16, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 3000000000000u128, 160000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 846853849005u128; - let exp_put = 15846853849005u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#424 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#424 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0425() { - // S=100.0, K=80.0, T=1.0, r=0.0 - // v0=0.16, kappa=3.0, theta=0.16, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 3000000000000u128, 160000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 26555698036473u128; - let exp_put = 6555698036473u128; - let tol = 325000000000u128; // $0.33 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#425 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#425 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0426() { - // S=100.0, K=90.0, T=0.1, r=0.0 - // v0=0.16, kappa=3.0, theta=0.16, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 3000000000000u128, 160000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 11493031396815u128; - let exp_put = 1493031396815u128; - let tol = 583333333333u128; // $0.58 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#426 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#426 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0427() { - // S=100.0, K=100.0, T=0.5, r=0.0 - // v0=0.16, kappa=5.0, theta=0.01, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 5000000000000u128, 10000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 7156513229036u128; - let exp_put = 7156513229036u128; - let tol = 1300000000000u128; // $1.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#427 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#427 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0428() { - // S=100.0, K=110.0, T=2.0, r=0.0 - // v0=0.16, kappa=5.0, theta=0.01, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 5000000000000u128, 10000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 4775751942470u128; - let exp_put = 14775751942470u128; - let tol = 375000000000u128; // $0.38 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#428 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#428 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0429() { - // S=100.0, K=120.0, T=0.25, r=0.0 - // v0=0.16, kappa=5.0, theta=0.01, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 5000000000000u128, 10000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 611794535394u128; - let exp_put = 20611794535394u128; - let tol = 375000000000u128; // $0.38 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#429 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#429 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0430() { - // S=100.0, K=85.0, T=2.0, r=0.0 - // v0=0.16, kappa=5.0, theta=0.01, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 5000000000000u128, 10000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 17779477797136u128; - let exp_put = 2779477797136u128; - let tol = 375000000000u128; // $0.38 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#430 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#430 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0431() { - // S=100.0, K=95.0, T=0.25, r=0.0 - // v0=0.16, kappa=5.0, theta=0.01, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 5000000000000u128, 10000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 8758949923748u128; - let exp_put = 3758949923748u128; - let tol = 375000000000u128; // $0.38 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#431 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#431 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0432() { - // S=100.0, K=105.0, T=1.0, r=0.0 - // v0=0.16, kappa=5.0, theta=0.01, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 5000000000000u128, 10000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 5177603511983u128; - let exp_put = 10177603511983u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#432 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#432 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0433() { - // S=100.0, K=115.0, T=0.1, r=0.0 - // v0=0.16, kappa=5.0, theta=0.04, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 5000000000000u128, 40000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 658180705875u128; - let exp_put = 15658180705875u128; - let tol = 1210000000000u128; // $1.21 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#433 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#433 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0434() { - // S=100.0, K=80.0, T=1.0, r=0.0 - // v0=0.16, kappa=5.0, theta=0.04, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 5000000000000u128, 40000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 22472832731115u128; - let exp_put = 2472832731115u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#434 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#434 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0435() { - // S=100.0, K=90.0, T=0.1, r=0.0 - // v0=0.16, kappa=5.0, theta=0.04, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 5000000000000u128, 40000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 11136896468768u128; - let exp_put = 1136896468768u128; - let tol = 1210000000000u128; // $1.21 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#435 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#435 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0436() { - // S=100.0, K=100.0, T=0.5, r=0.0 - // v0=0.16, kappa=5.0, theta=0.04, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 5000000000000u128, 40000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 7903924636624u128; - let exp_put = 7903924636624u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#436 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#436 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0437() { - // S=100.0, K=110.0, T=2.0, r=0.0 - // v0=0.16, kappa=5.0, theta=0.04, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 5000000000000u128, 40000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 7413218256048u128; - let exp_put = 17413218256048u128; - let tol = 750000000000u128; // $0.75 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#437 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#437 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0438() { - // S=100.0, K=120.0, T=0.25, r=0.0 - // v0=0.16, kappa=5.0, theta=0.09, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 5000000000000u128, 90000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 1481370574679u128; - let exp_put = 21481370574679u128; - let tol = 1060000000000u128; // $1.06 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#438 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#438 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0439() { - // S=100.0, K=85.0, T=2.0, r=0.0 - // v0=0.16, kappa=5.0, theta=0.09, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 5000000000000u128, 90000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 24685191506216u128; - let exp_put = 9685191506216u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#439 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#439 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0440() { - // S=100.0, K=95.0, T=0.25, r=0.0 - // v0=0.16, kappa=5.0, theta=0.09, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 5000000000000u128, 90000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 9765288303972u128; - let exp_put = 4765288303972u128; - let tol = 1060000000000u128; // $1.06 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#440 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#440 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0441() { - // S=100.0, K=105.0, T=1.0, r=0.0 - // v0=0.16, kappa=5.0, theta=0.09, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 5000000000000u128, 90000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 10638529781921u128; - let exp_put = 15638529781921u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#441 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#441 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0442() { - // S=100.0, K=115.0, T=0.1, r=0.0 - // v0=0.16, kappa=5.0, theta=0.09, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 5000000000000u128, 90000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 686765451395u128; - let exp_put = 15686765451395u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#442 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#442 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0443() { - // S=100.0, K=80.0, T=1.0, r=0.0 - // v0=0.16, kappa=5.0, theta=0.09, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 1000000000000u128, - 160000000000u128, 5000000000000u128, 90000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 24418850658714u128; - let exp_put = 4418850658714u128; - let tol = 625000000000u128; // $0.62 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#443 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#443 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0444() { - // S=100.0, K=90.0, T=0.1, r=0.0 - // v0=0.16, kappa=5.0, theta=0.16, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 100000000000u128, - 160000000000u128, 5000000000000u128, 160000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 11357146283813u128; - let exp_put = 1357146283813u128; - let tol = 850000000000u128; // $0.85 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#444 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#444 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0445() { - // S=100.0, K=100.0, T=0.5, r=0.0 - // v0=0.16, kappa=5.0, theta=0.16, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 5000000000000u128, 160000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 11166330548915u128; - let exp_put = 11166330548915u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#445 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#445 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0446() { - // S=100.0, K=115.0, T=2.0, r=0.0 - // v0=0.16, kappa=5.0, theta=0.16, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 5000000000000u128, 160000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 16595265953463u128; - let exp_put = 31595265953463u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#446 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#446 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0447() { - // S=100.0, K=80.0, T=0.5, r=0.0 - // v0=0.16, kappa=5.0, theta=0.16, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 500000000000u128, - 160000000000u128, 5000000000000u128, 160000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 23460703357625u128; - let exp_put = 3460703357625u128; - let tol = 525000000000u128; // $0.53 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#447 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#447 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0448() { - // S=100.0, K=90.0, T=2.0, r=0.0 - // v0=0.16, kappa=5.0, theta=0.16, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 2000000000000u128, - 160000000000u128, 5000000000000u128, 160000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 26443487852861u128; - let exp_put = 16443487852861u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#448 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#448 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0449() { - // S=100.0, K=100.0, T=0.25, r=0.0 - // v0=0.16, kappa=5.0, theta=0.16, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 250000000000u128, - 160000000000u128, 5000000000000u128, 160000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 7815125559061u128; - let exp_put = 7815125559061u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#449 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#449 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0450() { - // S=100.0, K=110.0, T=1.0, r=0.0 - // v0=0.25, kappa=0.5, theta=0.01, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 1000000000000u128, - 250000000000u128, 500000000000u128, 10000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 13792508005768u128; - let exp_put = 23792508005768u128; - let tol = 895000000000u128; // $0.89 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#450 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#450 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0451() { - // S=100.0, K=120.0, T=0.1, r=0.0 - // v0=0.25, kappa=0.5, theta=0.01, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 100000000000u128, - 250000000000u128, 500000000000u128, 10000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 962439813664u128; - let exp_put = 20962439813664u128; - let tol = 895000000000u128; // $0.89 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#451 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#451 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0452() { - // S=100.0, K=85.0, T=1.0, r=0.0 - // v0=0.25, kappa=0.5, theta=0.01, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 1000000000000u128, - 250000000000u128, 500000000000u128, 10000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 24845018538156u128; - let exp_put = 9845018538156u128; - let tol = 102941176471u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#452 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#452 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0453() { - // S=100.0, K=95.0, T=0.1, r=0.0 - // v0=0.25, kappa=0.5, theta=0.01, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 100000000000u128, - 250000000000u128, 500000000000u128, 10000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 8892357892924u128; - let exp_put = 3892357892924u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#453 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#453 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0454() { - // S=100.0, K=105.0, T=0.5, r=0.0 - // v0=0.25, kappa=0.5, theta=0.01, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 500000000000u128, - 250000000000u128, 500000000000u128, 10000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 9668731672095u128; - let exp_put = 14668731672095u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#454 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#454 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0455() { - // S=100.0, K=115.0, T=2.0, r=0.0 - // v0=0.25, kappa=0.5, theta=0.04, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 2000000000000u128, - 250000000000u128, 500000000000u128, 40000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 17189273146232u128; - let exp_put = 32189273146232u128; - let tol = 289130434783u128; // $0.29 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#455 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#455 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0456() { - // S=100.0, K=80.0, T=0.5, r=0.0 - // v0=0.25, kappa=0.5, theta=0.04, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 500000000000u128, - 250000000000u128, 500000000000u128, 40000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 24779105427703u128; - let exp_put = 4779105427703u128; - let tol = 325000000000u128; // $0.33 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#456 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#456 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0457() { - // S=100.0, K=90.0, T=2.0, r=0.0 - // v0=0.25, kappa=0.5, theta=0.04, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 2000000000000u128, - 250000000000u128, 500000000000u128, 40000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 27054117749420u128; - let exp_put = 17054117749420u128; - let tol = 83333333333u128; // $0.08 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#457 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#457 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0458() { - // S=100.0, K=100.0, T=0.25, r=0.0 - // v0=0.25, kappa=0.5, theta=0.04, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 250000000000u128, - 250000000000u128, 500000000000u128, 40000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 9646528505959u128; - let exp_put = 9646528505959u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#458 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#458 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0459() { - // S=100.0, K=110.0, T=1.0, r=0.0 - // v0=0.25, kappa=0.5, theta=0.04, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 1000000000000u128, - 250000000000u128, 500000000000u128, 40000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 12949478746992u128; - let exp_put = 22949478746992u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#459 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#459 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0460() { - // S=100.0, K=120.0, T=0.1, r=0.0 - // v0=0.25, kappa=0.5, theta=0.04, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 100000000000u128, - 250000000000u128, 500000000000u128, 40000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 861265612256u128; - let exp_put = 20861265612256u128; - let tol = 600000000000u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#460 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#460 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0461() { - // S=100.0, K=85.0, T=1.0, r=0.0 - // v0=0.25, kappa=0.5, theta=0.09, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 1000000000000u128, - 250000000000u128, 500000000000u128, 90000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 25525107014758u128; - let exp_put = 10525107014758u128; - let tol = 655000000000u128; // $0.65 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#461 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#461 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0462() { - // S=100.0, K=95.0, T=0.1, r=0.0 - // v0=0.25, kappa=0.5, theta=0.09, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 100000000000u128, - 250000000000u128, 500000000000u128, 90000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 8897764581935u128; - let exp_put = 3897764581935u128; - let tol = 655000000000u128; // $0.65 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#462 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#462 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0463() { - // S=100.0, K=105.0, T=0.5, r=0.0 - // v0=0.25, kappa=0.5, theta=0.09, xi=0.3, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 500000000000u128, - 250000000000u128, 500000000000u128, 90000000000u128, 300000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 11074108582625u128; - let exp_put = 16074108582625u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#463 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#463 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0464() { - // S=100.0, K=115.0, T=2.0, r=0.0 - // v0=0.25, kappa=0.5, theta=0.09, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 2000000000000u128, - 250000000000u128, 500000000000u128, 90000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 13933436807140u128; - let exp_put = 28933436807140u128; - let tol = 489130434783u128; // $0.49 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#464 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#464 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0465() { - // S=100.0, K=80.0, T=0.5, r=0.0 - // v0=0.25, kappa=0.5, theta=0.09, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 500000000000u128, - 250000000000u128, 500000000000u128, 90000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 25303252304328u128; - let exp_put = 5303252304328u128; - let tol = 825000000000u128; // $0.83 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#465 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#465 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0466() { - // S=100.0, K=90.0, T=2.0, r=0.0 - // v0=0.25, kappa=0.5, theta=0.09, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 2000000000000u128, - 250000000000u128, 500000000000u128, 90000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 25602498394389u128; - let exp_put = 15602498394389u128; - let tol = 583333333333u128; // $0.58 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#466 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#466 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0467() { - // S=100.0, K=100.0, T=0.25, r=0.0 - // v0=0.25, kappa=0.5, theta=0.16, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 250000000000u128, - 250000000000u128, 500000000000u128, 160000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 9823199644115u128; - let exp_put = 9823199644115u128; - let tol = 445000000000u128; // $0.45 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#467 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#467 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0468() { - // S=100.0, K=110.0, T=1.0, r=0.0 - // v0=0.25, kappa=0.5, theta=0.16, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 1000000000000u128, - 250000000000u128, 500000000000u128, 160000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 14987130945306u128; - let exp_put = 24987130945306u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#468 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#468 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0469() { - // S=100.0, K=120.0, T=0.1, r=0.0 - // v0=0.25, kappa=0.5, theta=0.16, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 100000000000u128, - 250000000000u128, 500000000000u128, 160000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 968482995312u128; - let exp_put = 20968482995312u128; - let tol = 445000000000u128; // $0.45 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#469 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#469 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0470() { - // S=100.0, K=85.0, T=1.0, r=0.0 - // v0=0.25, kappa=0.5, theta=0.16, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 1000000000000u128, - 250000000000u128, 500000000000u128, 160000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 25751720226581u128; - let exp_put = 10751720226581u128; - let tol = 302941176471u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#470 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#470 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0471() { - // S=100.0, K=95.0, T=0.1, r=0.0 - // v0=0.25, kappa=0.5, theta=0.16, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 100000000000u128, - 250000000000u128, 500000000000u128, 160000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 8936890703257u128; - let exp_put = 3936890703257u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#471 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#471 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0472() { - // S=100.0, K=105.0, T=0.5, r=0.0 - // v0=0.25, kappa=1.0, theta=0.01, xi=0.1, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 500000000000u128, - 250000000000u128, 1000000000000u128, 10000000000u128, 100000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 10370840540075u128; - let exp_put = 15370840540075u128; - let tol = 970000000000u128; // $0.97 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#472 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#472 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0473() { - // S=100.0, K=115.0, T=2.0, r=0.0 - // v0=0.25, kappa=1.0, theta=0.01, xi=0.2, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 2000000000000u128, - 250000000000u128, 1000000000000u128, 10000000000u128, 200000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 12077290568236u128; - let exp_put = 27077290568236u128; - let tol = 289130434783u128; // $0.29 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#473 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#473 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0474() { - // S=100.0, K=80.0, T=0.5, r=0.0 - // v0=0.25, kappa=1.0, theta=0.01, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 500000000000u128, - 250000000000u128, 1000000000000u128, 10000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 24330195958399u128; - let exp_put = 4330195958399u128; - let tol = 325000000000u128; // $0.33 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#474 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#474 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0475() { - // S=100.0, K=90.0, T=2.0, r=0.0 - // v0=0.25, kappa=1.0, theta=0.01, xi=0.3, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 2000000000000u128, - 250000000000u128, 1000000000000u128, 10000000000u128, 300000000000u128, - 0i128, - ).unwrap(); - let exp_call = 22928823363539u128; - let exp_put = 12928823363539u128; - let tol = 83333333333u128; // $0.08 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#475 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#475 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0476() { - // S=100.0, K=100.0, T=0.25, r=0.0 - // v0=0.25, kappa=1.0, theta=0.01, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 250000000000u128, - 250000000000u128, 1000000000000u128, 10000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 9275479642567u128; - let exp_put = 9275479642567u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#476 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#476 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0477() { - // S=100.0, K=110.0, T=1.0, r=0.0 - // v0=0.25, kappa=1.0, theta=0.01, xi=0.8, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 1000000000000u128, - 250000000000u128, 1000000000000u128, 10000000000u128, 800000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 10013669260973u128; - let exp_put = 20013669260973u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#477 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#477 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0478() { - // S=100.0, K=120.0, T=0.1, r=0.0 - // v0=0.25, kappa=1.0, theta=0.04, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 100000000000u128, - 250000000000u128, 1000000000000u128, 40000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 953232960033u128; - let exp_put = 20953232960033u128; - let tol = 880000000000u128; // $0.88 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#478 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#478 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0479() { - // S=100.0, K=85.0, T=1.0, r=0.0 - // v0=0.25, kappa=1.0, theta=0.04, xi=0.2, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 1000000000000u128, - 250000000000u128, 1000000000000u128, 40000000000u128, 200000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 23910149802238u128; - let exp_put = 8910149802238u128; - let tol = 102941176471u128; // $0.10 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#479 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#479 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0480() { - // S=100.0, K=95.0, T=0.1, r=0.0 - // v0=0.25, kappa=1.0, theta=0.04, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 100000000000u128, - 250000000000u128, 1000000000000u128, 40000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 8833603068256u128; - let exp_put = 3833603068256u128; - let tol = 880000000000u128; // $0.88 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#480 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#480 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0481() { - // S=100.0, K=105.0, T=0.5, r=0.0 - // v0=0.25, kappa=1.0, theta=0.04, xi=0.5, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 500000000000u128, - 250000000000u128, 1000000000000u128, 40000000000u128, 500000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 9948355016333u128; - let exp_put = 14948355016333u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#481 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#481 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0482() { - // S=100.0, K=115.0, T=2.0, r=0.0 - // v0=0.25, kappa=1.0, theta=0.04, xi=0.8, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 2000000000000u128, - 250000000000u128, 1000000000000u128, 40000000000u128, 800000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 7883653730338u128; - let exp_put = 22883653730338u128; - let tol = 789130434783u128; // $0.79 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#482 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#482 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0483() { - // S=100.0, K=80.0, T=0.5, r=0.0 - // v0=0.25, kappa=1.0, theta=0.09, xi=0.1, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 500000000000u128, - 250000000000u128, 1000000000000u128, 90000000000u128, 100000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 24441495697698u128; - let exp_put = 4441495697698u128; - let tol = 730000000000u128; // $0.73 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#483 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#483 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0484() { - // S=100.0, K=90.0, T=2.0, r=0.0 - // v0=0.25, kappa=1.0, theta=0.09, xi=0.1, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 2000000000000u128, - 250000000000u128, 1000000000000u128, 90000000000u128, 100000000000u128, - 0i128, - ).unwrap(); - let exp_call = 26424299815644u128; - let exp_put = 16424299815644u128; - let tol = 83333333333u128; // $0.08 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#484 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#484 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0485() { - // S=100.0, K=100.0, T=0.25, r=0.0 - // v0=0.25, kappa=1.0, theta=0.09, xi=0.2, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 250000000000u128, - 250000000000u128, 1000000000000u128, 90000000000u128, 200000000000u128, - 0i128, - ).unwrap(); - let exp_call = 9548669136428u128; - let exp_put = 9548669136428u128; - let tol = 730000000000u128; // $0.73 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#485 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#485 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0486() { - // S=100.0, K=110.0, T=1.0, r=0.0 - // v0=0.25, kappa=1.0, theta=0.09, xi=0.3, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 1000000000000u128, - 250000000000u128, 1000000000000u128, 90000000000u128, 300000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 13093065135260u128; - let exp_put = 23093065135260u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#486 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#486 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0487() { - // S=100.0, K=120.0, T=0.1, r=0.0 - // v0=0.25, kappa=1.0, theta=0.09, xi=0.5, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 120000000000000u128, 0u128, 100000000000u128, - 250000000000u128, 1000000000000u128, 90000000000u128, 500000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 895266824097u128; - let exp_put = 20895266824097u128; - let tol = 300000000000u128; // $0.30 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#487 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#487 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0488() { - // S=100.0, K=85.0, T=1.0, r=0.0 - // v0=0.25, kappa=1.0, theta=0.09, xi=0.8, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 85000000000000u128, 0u128, 1000000000000u128, - 250000000000u128, 1000000000000u128, 90000000000u128, 800000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 24004543634154u128; - let exp_put = 9004543634154u128; - let tol = 602941176471u128; // $0.60 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#488 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#488 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0489() { - // S=100.0, K=95.0, T=0.1, r=0.0 - // v0=0.25, kappa=1.0, theta=0.16, xi=0.1, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 95000000000000u128, 0u128, 100000000000u128, - 250000000000u128, 1000000000000u128, 160000000000u128, 100000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 8884017896851u128; - let exp_put = 3884017896851u128; - let tol = 520000000000u128; // $0.52 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#489 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#489 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0490() { - // S=100.0, K=105.0, T=0.5, r=0.0 - // v0=0.25, kappa=1.0, theta=0.16, xi=0.2, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 105000000000000u128, 0u128, 500000000000u128, - 250000000000u128, 1000000000000u128, 160000000000u128, 200000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 11225074665206u128; - let exp_put = 16225074665206u128; - let tol = 50000000000u128; // $0.05 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#490 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#490 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0491() { - // S=100.0, K=115.0, T=2.0, r=0.0 - // v0=0.25, kappa=1.0, theta=0.16, xi=0.3, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 115000000000000u128, 0u128, 2000000000000u128, - 250000000000u128, 1000000000000u128, 160000000000u128, 300000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 17723519092482u128; - let exp_put = 32723519092482u128; - let tol = 289130434783u128; // $0.29 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#491 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#491 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0492() { - // S=100.0, K=80.0, T=0.5, r=0.0 - // v0=0.25, kappa=1.0, theta=0.16, xi=0.5, rho=-0.9 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 500000000000u128, - 250000000000u128, 1000000000000u128, 160000000000u128, 500000000000u128, - -900000000000i128, - ).unwrap(); - let exp_call = 25115184639606u128; - let exp_put = 5115184639606u128; - let tol = 525000000000u128; // $0.53 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#492 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#492 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0493() { - // S=100.0, K=90.0, T=2.0, r=0.0 - // v0=0.25, kappa=1.0, theta=0.16, xi=0.5, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 2000000000000u128, - 250000000000u128, 1000000000000u128, 160000000000u128, 500000000000u128, - 0i128, - ).unwrap(); - let exp_call = 28125918603475u128; - let exp_put = 18125918603475u128; - let tol = 283333333333u128; // $0.28 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#493 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#493 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0494() { - // S=100.0, K=100.0, T=0.25, r=0.0 - // v0=0.25, kappa=1.0, theta=0.16, xi=0.8, rho=0.0 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 250000000000u128, - 250000000000u128, 1000000000000u128, 160000000000u128, 800000000000u128, - 0i128, - ).unwrap(); - let exp_call = 9497667766455u128; - let exp_put = 9497667766455u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#494 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#494 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0495() { - // S=100.0, K=110.0, T=1.0, r=0.0 - // v0=0.25, kappa=2.0, theta=0.01, xi=0.1, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 1000000000000u128, - 250000000000u128, 2000000000000u128, 10000000000u128, 100000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 9505218558504u128; - let exp_put = 19505218558504u128; - let tol = 1120000000000u128; // $1.12 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#495 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#495 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0496() { - // S=100.0, K=80.0, T=0.25, r=0.0 - // v0=0.25, kappa=2.0, theta=0.01, xi=0.2, rho=-0.3 - let (call, put) = heston_price( - 100000000000000u128, 80000000000000u128, 0u128, 250000000000u128, - 250000000000u128, 2000000000000u128, 10000000000u128, 200000000000u128, - -300000000000i128, - ).unwrap(); - let exp_call = 21711359696654u128; - let exp_put = 1711359696654u128; - let tol = 1120000000000u128; // $1.12 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#496 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#496 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0497() { - // S=100.0, K=90.0, T=1.0, r=0.0 - // v0=0.25, kappa=2.0, theta=0.01, xi=0.3, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 90000000000000u128, 0u128, 1000000000000u128, - 250000000000u128, 2000000000000u128, 10000000000u128, 300000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 18315487305291u128; - let exp_put = 8315487305291u128; - let tol = 240000000000u128; // $0.24 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#497 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#497 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0498() { - // S=100.0, K=100.0, T=0.1, r=0.0 - // v0=0.25, kappa=2.0, theta=0.01, xi=0.5, rho=-0.5 - let (call, put) = heston_price( - 100000000000000u128, 100000000000000u128, 0u128, 100000000000u128, - 250000000000u128, 2000000000000u128, 10000000000u128, 500000000000u128, - -500000000000i128, - ).unwrap(); - let exp_call = 5933630432005u128; - let exp_put = 5933630432005u128; - let tol = 250000000000u128; // $0.25 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#498 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#498 put: got={} exp={} diff={}", put, exp_put, dp); - } - - #[test] - fn ql_heston_0499() { - // S=100.0, K=110.0, T=0.5, r=0.0 - // v0=0.25, kappa=2.0, theta=0.01, xi=0.8, rho=-0.7 - let (call, put) = heston_price( - 100000000000000u128, 110000000000000u128, 0u128, 500000000000u128, - 250000000000u128, 2000000000000u128, 10000000000u128, 800000000000u128, - -700000000000i128, - ).unwrap(); - let exp_call = 5852729522769u128; - let exp_put = 15852729522769u128; - let tol = 550000000000u128; // $0.55 - let dc = if call > exp_call { call - exp_call } else { exp_call - call }; - let dp = if put > exp_put { put - exp_put } else { exp_put - put }; - assert!(dc <= tol, - "Heston#499 call: got={} exp={} diff={}", call, exp_call, dc); - assert!(dp <= tol, - "Heston#499 put: got={} exp={} diff={}", put, exp_put, dp); - } - -} diff --git a/test_data/sabr_reference_tests.rs b/test_data/sabr_reference_tests.rs index ce42dff..1d48ba9 100644 --- a/test_data/sabr_reference_tests.rs +++ b/test_data/sabr_reference_tests.rs @@ -3,7 +3,7 @@ #[cfg(test)] mod quantlib_sabr { - use crate::sabr::sabr_implied_vol; + use solmath::sabr_implied_vol; #[test] fn ql_sabr_0000() { diff --git a/tests/asian_quantlib_reference.rs b/tests/asian_quantlib_reference.rs new file mode 100644 index 0000000..e9d0631 --- /dev/null +++ b/tests/asian_quantlib_reference.rs @@ -0,0 +1,6557 @@ +//! Auto-generated QuantLib arithmetic-Asian reference tests. +//! Source: scripts/generate_asian_quantlib_vectors.py + +#![cfg(feature = "asian")] + +use solmath::arithmetic_asian_price; + +#[test] +fn matches_quantlib_1_41_continuous_arithmetic_levy_engine() { + const VECTORS: &[[u128; 11]] = &[ + [ + 100000000000000, + 100000000000000, + 50000000000, + 20000000000, + 400000000000, + 1000000000000, + 1000000000000, + 0, + 0, + 9641361495792, + 8200141259059, + ], + [ + 508700889991963, + 409020030481282, + 13228106665, + 12425871774, + 822189339646, + 19178082192, + 19178082192, + 0, + 0, + 99663077244057, + 3590243141, + ], + [ + 256614378551818, + 212645070369322, + 87346863624, + 105233816875, + 1087194076545, + 30136986301, + 30136986301, + 0, + 0, + 44227033944399, + 442288002746, + ], + [ + 469538131909345, + 411396062552645, + 84549327651, + 4875254082, + 1185762363888, + 249315068493, + 249315068493, + 0, + 0, + 95478821081634, + 33952974417744, + ], + [ + 415053555362305, + 382067730660125, + 75312510951, + 26591456477, + 1203633543927, + 712328767123, + 712328767123, + 0, + 0, + 112311346305202, + 74142734844710, + ], + [ + 220355606389841, + 177627972600198, + 26614653341, + 100034373710, + 928700174732, + 1583561643836, + 1583561643836, + 0, + 0, + 66666495720759, + 37520925275118, + ], + [ + 465379378979362, + 436379550347102, + 108660773945, + 91618248004, + 916042543119, + 3049315068493, + 3049315068493, + 0, + 0, + 144501379126262, + 114846402502228, + ], + [ + 348274795899103, + 331039601432445, + 8549008286, + 92942655286, + 1328382375295, + 19178082192, + 19178082192, + 0, + 0, + 24426684995150, + 7475960718528, + ], + [ + 310587756837234, + 262078386685374, + 39370804887, + 88818112777, + 851651164961, + 30136986301, + 30136986301, + 0, + 0, + 48438986698526, + 218168260003, + ], + [ + 438856215516641, + 374521038713277, + 117767463921, + 13687719301, + 1255483965639, + 158904109589, + 158904109589, + 0, + 0, + 87300403637327, + 20576492002864, + ], + [ + 332667495172076, + 274991243083457, + 110101174357, + 9411855595, + 1029362612090, + 789041095890, + 789041095890, + 0, + 0, + 99730677957379, + 34410909440328, + ], + [ + 250854968291117, + 205344598253550, + 102418531949, + 107278928265, + 1092783619683, + 1139726027397, + 1139726027397, + 0, + 0, + 78381895246239, + 38502692028809, + ], + [ + 109001368002892, + 90267385126378, + 51126765258, + 4961106199, + 856332512684, + 2643835616438, + 2643835616438, + 0, + 0, + 43079973774712, + 20659767474314, + ], + [ + 376806146329959, + 305092625651809, + 19563462497, + 16232708848, + 1292723543410, + 10958904110, + 10958904110, + 0, + 0, + 71733086026634, + 28063070163, + ], + [ + 551635906882621, + 477220339261614, + 5216544436, + 110988501151, + 937781650046, + 32876712329, + 32876712329, + 0, + 0, + 75069183472622, + 1624240914991, + ], + [ + 358817252977419, + 339637759684623, + 68465197526, + 117666459208, + 976212750114, + 194520547945, + 194520547945, + 0, + 0, + 43524729809613, + 26287923396906, + ], + [ + 248731878586149, + 221570308106755, + 32129873153, + 116802067237, + 1152834354084, + 517808219178, + 517808219178, + 0, + 0, + 55774257011299, + 34346043880774, + ], + [ + 229067564952062, + 188048789916058, + 30903810376, + 83151711512, + 1323184243052, + 1821917808219, + 1821917808219, + 0, + 0, + 99189248658499, + 70402545823420, + ], + [ + 207702299895117, + 181914196236208, + 97062319858, + 33582352557, + 1032571522698, + 2142465753425, + 2142465753425, + 0, + 0, + 79010299296467, + 46053498185358, + ], + [ + 445469464884308, + 421819624072191, + 58225178848, + 67607014046, + 901848485281, + 2739726027, + 2739726027, + 0, + 0, + 23740386179152, + 100041846826, + ], + [ + 411967646123302, + 332035671520557, + 18046822596, + 10867093747, + 1469625775621, + 49315068493, + 49315068493, + 0, + 0, + 84347472600579, + 4413728063330, + ], + [ + 208002195220608, + 174710052557650, + 106620471715, + 47779695242, + 1329790199260, + 438356164384, + 438356164384, + 0, + 0, + 57704072275567, + 23349941394805, + ], + [ + 234421765534328, + 191146859522325, + 85649872291, + 80408652313, + 1069709502605, + 761643835616, + 761643835616, + 0, + 0, + 67298604481750, + 26317680237587, + ], + [ + 177561842840178, + 148345984397884, + 80411119307, + 76417994855, + 862546368966, + 1638356164384, + 1638356164384, + 0, + 0, + 51995622390515, + 25875723957163, + ], + [ + 275825508158594, + 235059752844540, + 63692845282, + 68345558680, + 1021887068486, + 2293150684932, + 2293150684932, + 0, + 0, + 100965901819649, + 67006854222267, + ], + [ + 98903400962987, + 87788514938711, + 88822460932, + 78915498990, + 1004384037528, + 5479452055, + 5479452055, + 0, + 0, + 11115444795916, + 3283838712, + ], + [ + 472899935775301, + 382678898848399, + 53307943259, + 62768783123, + 1128861896885, + 57534246575, + 57534246575, + 0, + 0, + 92539701212450, + 2723238791097, + ], + [ + 500873717346562, + 451462010298558, + 15639442022, + 107678534747, + 1097068662541, + 224657534247, + 224657534247, + 0, + 0, + 81521863118635, + 37408266902472, + ], + [ + 523268463189038, + 431762657825428, + 47003693193, + 67638564841, + 1177213021214, + 610958904110, + 610958904110, + 0, + 0, + 148476664211022, + 62752924135109, + ], + [ + 479342891915211, + 389900508964334, + 119287739704, + 67624154980, + 1460827824694, + 1753424657534, + 1753424657534, + 0, + 0, + 224135732024710, + 133416562348387, + ], + [ + 323250436584330, + 307138630244668, + 110599604779, + 96878652509, + 905967388613, + 4030136986301, + 4030136986301, + 0, + 0, + 101087349760543, + 84939956712523, + ], + [ + 391184706198407, + 320992216714209, + 54355287632, + 20186950036, + 1134867437712, + 13698630137, + 13698630137, + 0, + 0, + 70274131567945, + 42392489188, + ], + [ + 226415063151511, + 202424579322860, + 101563150066, + 16408358387, + 1040276438567, + 43835616438, + 43835616438, + 0, + 0, + 26986214037244, + 2681071144156, + ], + [ + 552989127381396, + 476439471241090, + 72094569260, + 106345646111, + 889001192597, + 249315068493, + 249315068493, + 0, + 0, + 96106673613301, + 23233075638133, + ], + [ + 375166994877973, + 301612136049097, + 108200428709, + 88871650698, + 1020472519400, + 580821917808, + 580821917808, + 0, + 0, + 100220014109603, + 29160397944645, + ], + [ + 431208712032616, + 365320229909974, + 84972528036, + 73392269381, + 1415169786158, + 1835616438356, + 1835616438356, + 0, + 0, + 195633296059811, + 135311379794165, + ], + [ + 589155950483814, + 486458478739258, + 103673553674, + 92499231933, + 1310809063153, + 4805479452055, + 4805479452055, + 0, + 0, + 277239709509781, + 205052497010265, + ], + [ + 236828682177178, + 219779529751167, + 82434707066, + 64139594812, + 1482601771930, + 19178082192, + 19178082192, + 0, + 0, + 21399667847408, + 4335961119277, + ], + [ + 585689982666952, + 495632944965561, + 117939895445, + 81687992241, + 1200794331658, + 54794520548, + 54794520548, + 0, + 0, + 96885683757594, + 6830414887202, + ], + [ + 433095578644492, + 355329184218888, + 9104461390, + 14297833468, + 1387822132442, + 109589041096, + 109589041096, + 0, + 0, + 91519498936871, + 13953756198975, + ], + [ + 361421273716163, + 316725770210921, + 63203029609, + 36480544134, + 983084527965, + 531506849315, + 531506849315, + 0, + 0, + 80949622426744, + 35236960051295, + ], + [ + 178830870206652, + 146674517360850, + 5302657475, + 107379071627, + 972839152027, + 1336986301370, + 1336986301370, + 0, + 0, + 51982452575519, + 31637106220411, + ], + [ + 57642502031281, + 50685078785486, + 72117503102, + 82411461639, + 900286610820, + 2345205479452, + 2345205479452, + 0, + 0, + 17874615029229, + 12582597085269, + ], + [ + 132393673727773, + 115051156054913, + 84878153510, + 115360181449, + 1446574934441, + 13698630137, + 13698630137, + 0, + 0, + 17705942539891, + 411182867110, + ], + [ + 508718535657792, + 428224943802477, + 67303321589, + 111403519073, + 1219698127597, + 35616438356, + 35616438356, + 0, + 0, + 82795263899731, + 2892747448462, + ], + [ + 366498217197928, + 299518309124665, + 116266320234, + 51635801593, + 1124328997376, + 323287671233, + 323287671233, + 0, + 0, + 89605758013897, + 21383302245210, + ], + [ + 411811211895671, + 377082944716088, + 34206813412, + 36794255611, + 1226651543773, + 928767123288, + 928767123288, + 0, + 0, + 125122981654704, + 91959656068560, + ], + [ + 483190871095761, + 407721743253859, + 33670670933, + 20795139283, + 1091899900166, + 1597260273973, + 1597260273973, + 0, + 0, + 182877907903440, + 106619521629199, + ], + [ + 156095742534630, + 134886408583266, + 73704750183, + 96959239084, + 1334357066924, + 2547945205479, + 2547945205479, + 0, + 0, + 70388576094376, + 56568681829050, + ], + [ + 499614686247270, + 405282934840309, + 18198994498, + 266748229, + 1076668045560, + 19178082192, + 19178082192, + 0, + 0, + 94480830615535, + 96107168629, + ], + [ + 109642850193495, + 95961062416036, + 8469897755, + 72654986716, + 1145543163031, + 68493150685, + 68493150685, + 0, + 0, + 15740891734412, + 2307554596382, + ], + [ + 230302192163753, + 196664565224464, + 90600899979, + 97338977220, + 945292856582, + 336986301370, + 336986301370, + 0, + 0, + 45605213432705, + 13232478794206, + ], + [ + 304698522924968, + 260955586753238, + 90699435000, + 92671540459, + 1250140346195, + 928767123288, + 928767123288, + 0, + 0, + 96380328654547, + 56427649823465, + ], + [ + 543761560211478, + 448876454262970, + 96814535484, + 11451100416, + 1488457981056, + 1890410958904, + 1890410958904, + 0, + 0, + 287347667158155, + 169748562354469, + ], + [ + 99343858613867, + 87189453527239, + 113248140547, + 116566239747, + 1034594463784, + 3843835616438, + 3843835616438, + 0, + 0, + 34373463479838, + 26916959701364, + ], + [ + 141881804125272, + 134351915598963, + 74518008180, + 61660682209, + 853143679595, + 19178082192, + 19178082192, + 0, + 0, + 8665942941599, + 1129338806099, + ], + [ + 213332741174987, + 195334823020814, + 61660775102, + 48843789733, + 1389033861252, + 63013698630, + 63013698630, + 0, + 0, + 26953430705704, + 8939469645885, + ], + [ + 257944651398518, + 217467517782485, + 54967301149, + 76124415975, + 1014767124304, + 164383561644, + 164383561644, + 0, + 0, + 47559995686233, + 7890955666309, + ], + [ + 124387173627666, + 102073013883780, + 119683344382, + 117698254945, + 1157533981815, + 884931506849, + 884931506849, + 0, + 0, + 37593975284203, + 17423963446902, + ], + [ + 238291860670618, + 215204006872616, + 49297561758, + 86181262973, + 1473218438688, + 1989041095890, + 1989041095890, + 0, + 0, + 111283826424667, + 98086627674938, + ], + [ + 341337057970716, + 290140875673290, + 77828717992, + 25858311801, + 1385018932460, + 4342465753425, + 4342465753425, + 0, + 0, + 209856590898049, + 143683391487500, + ], + [ + 314527868931421, + 297940150433417, + 115829486417, + 35246139582, + 1108593007267, + 8219178082, + 8219178082, + 0, + 0, + 18335289432922, + 1659271005246, + ], + [ + 456643457576258, + 425195286647706, + 107763504865, + 37985173361, + 1163667389696, + 82191780822, + 82191780822, + 0, + 0, + 52436191503241, + 19964926005209, + ], + [ + 582200986325787, + 471338668127456, + 43313091267, + 47748955940, + 884239814084, + 471232876712, + 471232876712, + 0, + 0, + 139011700924314, + 30984993800144, + ], + [ + 97199583906478, + 85754126223968, + 119233069267, + 108064795025, + 1097805169046, + 967123287671, + 967123287671, + 0, + 0, + 26723859264941, + 16055511312715, + ], + [ + 89333806425965, + 73201575698893, + 67426845041, + 886220602, + 984866191140, + 1945205479452, + 1945205479452, + 0, + 0, + 35662336404342, + 16216260296135, + ], + [ + 363691264204662, + 312813105106694, + 6461124054, + 70024943696, + 965836688139, + 4093150684932, + 4093150684932, + 0, + 0, + 150490732292012, + 143268477456509, + ], + [ + 316501672486913, + 270605562603660, + 45779456190, + 78005001872, + 1289122733684, + 10958904110, + 10958904110, + 0, + 0, + 46007126063787, + 189888878720, + ], + [ + 41256265514386, + 34983547125543, + 93558308790, + 88487865092, + 943768938064, + 63013698630, + 63013698630, + 0, + 0, + 6530054854558, + 287655403081, + ], + [ + 355566012270077, + 293699658157266, + 2569745105, + 83949266715, + 1450754519027, + 246575342466, + 246575342466, + 0, + 0, + 88090544771862, + 29804818371834, + ], + [ + 372156831842956, + 305026694039214, + 11868305670, + 116501113748, + 1140753551179, + 978082191781, + 978082191781, + 0, + 0, + 112484180200336, + 64326159335715, + ], + [ + 141151299917543, + 123035934215476, + 92975972792, + 58433431378, + 1096978641398, + 1134246575342, + 1134246575342, + 0, + 0, + 43827691373593, + 25004245915355, + ], + [ + 209988268346707, + 175660247126076, + 89022410623, + 99334750014, + 897122726620, + 4912328767123, + 4912328767123, + 0, + 0, + 70561535001807, + 51770862916803, + ], + [ + 514397779587101, + 475323634141440, + 36026701026, + 96988123860, + 1248158341041, + 10958904110, + 10958904110, + 0, + 0, + 41752240973042, + 2865240362384, + ], + [ + 410015547459438, + 375345580160572, + 28347473945, + 50483144025, + 1108219981710, + 73972602740, + 73972602740, + 0, + 0, + 47729062582645, + 13466520411924, + ], + [ + 244118726317516, + 195380474564284, + 55821500029, + 42511549372, + 1165005172196, + 260273972603, + 260273972603, + 0, + 0, + 60128215554970, + 11675732166969, + ], + [ + 149772188121899, + 123283305344677, + 99057026552, + 60811054557, + 1162375153066, + 731506849315, + 731506849315, + 0, + 0, + 45230162266597, + 18625836481943, + ], + [ + 533863944601809, + 497878844729194, + 17958830206, + 108719102916, + 1168668478643, + 1698630136986, + 1698630136986, + 0, + 0, + 174262864351235, + 177300343413705, + ], + [ + 120292859447727, + 99478111306449, + 52553689445, + 13964987158, + 1499478652962, + 2142465753425, + 2142465753425, + 0, + 0, + 68061205602690, + 44894855564698, + ], + [ + 438882898960522, + 399021162791109, + 32503188843, + 93589889924, + 1080219559788, + 8219178082, + 8219178082, + 0, + 0, + 40196103892177, + 455145187054, + ], + [ + 433741185827608, + 366463450691111, + 13241230119, + 96430596897, + 1082388509924, + 49315068493, + 49315068493, + 0, + 0, + 69461484057499, + 3115580288581, + ], + [ + 526598752748292, + 448714919383372, + 60288522724, + 12220312347, + 845241912728, + 446575342466, + 446575342466, + 0, + 0, + 111577736893368, + 30221378478035, + ], + [ + 449492877473898, + 420160622144487, + 106484943700, + 47222157737, + 1099304852699, + 616438356164, + 616438356164, + 0, + 0, + 102500526811566, + 67248589731341, + ], + [ + 252814662465618, + 220067114074152, + 61642216640, + 8057809809, + 1133717300225, + 1890410958904, + 1890410958904, + 0, + 0, + 105061776911796, + 64125407833993, + ], + [ + 319473018559992, + 262384963905441, + 4687767312, + 53924697305, + 1263929607186, + 2309589041096, + 2309589041096, + 0, + 0, + 152587277740626, + 113420932100565, + ], + [ + 541245562117223, + 439953551307452, + 114940140905, + 18870963636, + 1397499835701, + 5479452055, + 5479452055, + 0, + 0, + 101372533708871, + 1904563907, + ], + [ + 424478926264098, + 350638501123954, + 62591030526, + 73870156737, + 1293665959799, + 24657534247, + 24657534247, + 0, + 0, + 74656410378729, + 988788840692, + ], + [ + 114424242347089, + 101870819958368, + 14341364725, + 54110138287, + 807979640903, + 334246575342, + 334246575342, + 0, + 0, + 18383113803500, + 6643240211362, + ], + [ + 207776241355884, + 186005988359045, + 1503656206, + 99449243018, + 1265389847435, + 849315068493, + 849315068493, + 0, + 0, + 59959441271232, + 46613614746361, + ], + [ + 284636103169050, + 247404803078481, + 42326447459, + 43492653436, + 1358163188805, + 1169863013699, + 1169863013699, + 0, + 0, + 108877476627768, + 73629528917958, + ], + [ + 108448324323726, + 87680711103801, + 50622606025, + 117790430154, + 1315979544060, + 4613698630137, + 4613698630137, + 0, + 0, + 52469742239922, + 48057378239920, + ], + [ + 207888319006611, + 185904675300106, + 87170984200, + 58033286647, + 1476863691894, + 2739726027, + 2739726027, + 0, + 0, + 22004027209938, + 17337082029, + ], + [ + 428805008125310, + 400192491584730, + 51207171703, + 76480208732, + 839888361634, + 60273972603, + 60273972603, + 0, + 0, + 36867144213119, + 8668231850679, + ], + [ + 475158969358082, + 423454938135364, + 117028509384, + 65791428357, + 1379881894273, + 482191780822, + 482191780822, + 0, + 0, + 126490829266443, + 72030092093148, + ], + [ + 467067808982614, + 413162210523503, + 113031404841, + 50041689326, + 1441684341052, + 679452054795, + 679452054795, + 0, + 0, + 149466571740119, + 90156408067476, + ], + [ + 302723103877295, + 256811571194421, + 116489677849, + 55464921463, + 993934178358, + 1600000000000, + 1600000000000, + 0, + 0, + 100660987382144, + 49881509984046, + ], + [ + 220083157292075, + 188679568162642, + 7259038132, + 60355669563, + 1169289624085, + 2172602739726, + 2172602739726, + 0, + 0, + 91744569067951, + 72860952972833, + ], + [ + 101208373348089, + 91413274315794, + 36229450970, + 19221827009, + 1434742012258, + 5479452055, + 5479452055, + 0, + 0, + 9916682019547, + 118812154285, + ], + [ + 89375507840826, + 84437225501424, + 46126723276, + 78428537361, + 883881582242, + 24657534247, + 24657534247, + 0, + 0, + 5884211242971, + 987085514652, + ], + [ + 41292510591764, + 34214522278576, + 49046171058, + 8441888962, + 1174779001432, + 326027397260, + 326027397260, + 0, + 0, + 10142694634958, + 2906813518063, + ], + [ + 143130085953290, + 129757972980021, + 85415143854, + 86317626390, + 1434983737365, + 931506849315, + 931506849315, + 0, + 0, + 48568023689652, + 36274179815127, + ], + [ + 50318360455481, + 47820765157042, + 29972544195, + 107642741745, + 1490265279366, + 1405479452055, + 1405479452055, + 0, + 0, + 19712361891144, + 19857716400309, + ], + [ + 150158061873887, + 121831923928553, + 4299081082, + 17781890849, + 859791187120, + 2969863013699, + 2969863013699, + 0, + 0, + 61061064009547, + 36023244378287, + ], + [ + 295340606629229, + 253774798533874, + 13738938686, + 16874721976, + 1019538850629, + 2739726027, + 2739726027, + 0, + 0, + 41562975617694, + 681839, + ], + [ + 107202281932382, + 90020976106785, + 49774881306, + 82445545127, + 1060513135097, + 71232876712, + 71232876712, + 0, + 0, + 18187336347889, + 1191044892137, + ], + [ + 213537331728588, + 193917715506129, + 116951703536, + 8687371678, + 1363148319417, + 347945205479, + 347945205479, + 0, + 0, + 49833230376704, + 27085414780150, + ], + [ + 98897388240087, + 83553920137023, + 59001747093, + 61134078943, + 1075271464970, + 750684931507, + 750684931507, + 0, + 0, + 27293708759027, + 12690682255448, + ], + [ + 279518381839278, + 232018588758881, + 60999577812, + 11061994817, + 1030192872440, + 1290410958904, + 1290410958904, + 0, + 0, + 96875379795037, + 44465061256940, + ], + [ + 236161902323645, + 206758807258238, + 3481000119, + 96466249103, + 1229307361053, + 3849315068493, + 3849315068493, + 0, + 0, + 115070333138987, + 123200389455751, + ], + [ + 198935804838664, + 177109334992687, + 105886015841, + 78756403991, + 1181648599420, + 2739726027, + 2739726027, + 0, + 0, + 21828541254202, + 1011060307, + ], + [ + 34790444521908, + 28739845136134, + 111468600545, + 33155254047, + 1230502524209, + 71232876712, + 71232876712, + 0, + 0, + 6580655568689, + 481458303940, + ], + [ + 300220116669607, + 260436366849383, + 68563249792, + 118120018584, + 929290909225, + 246575342466, + 246575342466, + 0, + 0, + 51648313604602, + 14327688027858, + ], + [ + 467215824333405, + 396279211632368, + 18535137750, + 45679095723, + 1221052609298, + 589041095890, + 589041095890, + 0, + 0, + 130577424047644, + 64086046347347, + ], + [ + 369085280728842, + 319173165840000, + 9404513040, + 52387745157, + 1463780657255, + 1416438356164, + 1416438356164, + 0, + 0, + 162740849785860, + 124354436548409, + ], + [ + 503570343450428, + 423928324717635, + 71662862428, + 70782356089, + 961671823533, + 2624657534247, + 2624657534247, + 0, + 0, + 181621286503272, + 115152224294014, + ], + [ + 28705354380595, + 25088796881870, + 72279101777, + 77041868365, + 1033855534443, + 19178082192, + 19178082192, + 0, + 0, + 3658526267902, + 48287604338, + ], + [ + 386763536775780, + 366240486351539, + 112059377453, + 92444467488, + 1380997437604, + 35616438356, + 35616438356, + 0, + 0, + 34319553370499, + 13743658122169, + ], + [ + 145248925644896, + 138068745797393, + 116595741538, + 48389781039, + 935910143215, + 419178082192, + 419178082192, + 0, + 0, + 23958604960996, + 15124610105059, + ], + [ + 205467200322962, + 164427171672592, + 84947575067, + 89743062976, + 1220634029108, + 621917808219, + 621917808219, + 0, + 0, + 61938750108344, + 23300943163362, + ], + [ + 245773272398756, + 202394579144625, + 77734474984, + 103068822913, + 1176934525547, + 1772602739726, + 1772602739726, + 0, + 0, + 91044798006437, + 57986862229108, + ], + [ + 501376016527506, + 442863716180189, + 39218149214, + 61269256311, + 1372295424820, + 3641095890411, + 3641095890411, + 0, + 0, + 281668481890605, + 247934018179067, + ], + [ + 309715676493002, + 278931184271188, + 53519462982, + 9368184915, + 855807816840, + 8219178082, + 8219178082, + 0, + 0, + 30869915544163, + 42784000693, + ], + [ + 312887157953605, + 265097513305440, + 116925171592, + 81232965686, + 823717696524, + 73972602740, + 73972602740, + 0, + 0, + 49513718208653, + 1725783272383, + ], + [ + 531034971452498, + 456352752968532, + 86286496384, + 85063851800, + 1050136592721, + 117808219178, + 117808219178, + 0, + 0, + 87889710616154, + 13924951160305, + ], + [ + 267757844916918, + 245182270040767, + 14842354857, + 108858388317, + 1192566118646, + 789041095890, + 789041095890, + 0, + 0, + 68524395738822, + 55789224265118, + ], + [ + 189535656602758, + 158382320903614, + 13338959863, + 36513249290, + 1032227036042, + 1230136986301, + 1230136986301, + 0, + 0, + 61437214872602, + 33423450585221, + ], + [ + 179517058126073, + 155578608296258, + 97533438779, + 9392563551, + 884690126068, + 4430136986301, + 4430136986301, + 0, + 0, + 80930388487254, + 39364204527878, + ], + [ + 101527656690212, + 92128135486348, + 77853661079, + 34712408423, + 1240464344541, + 8219178082, + 8219178082, + 0, + 0, + 9596246436339, + 184747260937, + ], + [ + 283768925982002, + 243348237540813, + 62325980720, + 41030744639, + 1310164506122, + 60273972603, + 60273972603, + 0, + 0, + 46031983720224, + 5581345793413, + ], + [ + 544630342792992, + 480063745769167, + 13354028848, + 63957083670, + 829712773931, + 487671232877, + 487671232877, + 0, + 0, + 100443072919139, + 42917486848423, + ], + [ + 167698203117149, + 139582197882223, + 9084128118, + 77365586878, + 1389146706690, + 739726027397, + 739726027397, + 0, + 0, + 55600808002720, + 31809977815229, + ], + [ + 540079965574774, + 456191181378327, + 36822136069, + 45800569919, + 1043745497249, + 1213698630137, + 1213698630137, + 0, + 0, + 172130451665825, + 94712045961334, + ], + [ + 135473773797691, + 115048805008561, + 72439811529, + 90263082979, + 1404968269060, + 4690410958904, + 4690410958904, + 0, + 0, + 71352146249008, + 60732378465869, + ], + [ + 519395243425730, + 466094401313863, + 110211103232, + 42569820899, + 1498885608109, + 19178082192, + 19178082192, + 0, + 0, + 59364099887763, + 5839475792285, + ], + [ + 114558117372938, + 102368649084294, + 17260351546, + 77306337663, + 1080663569874, + 63013698630, + 63013698630, + 0, + 0, + 14362460984214, + 2402462741558, + ], + [ + 529256220978602, + 476488068489986, + 24354042876, + 70754289923, + 849090915062, + 301369863014, + 301369863014, + 0, + 0, + 81434252368785, + 32708317829835, + ], + [ + 422198871388695, + 370012105166251, + 96491347442, + 30514283109, + 1190079291809, + 997260273973, + 997260273973, + 0, + 0, + 137443674496636, + 77148039401414, + ], + [ + 379476691945960, + 330224946855512, + 53726745216, + 37845032573, + 1150269811787, + 1605479452055, + 1605479452055, + 0, + 0, + 142586643686114, + 92929095166204, + ], + [ + 157131003913634, + 139270028844431, + 113325722874, + 106225534989, + 1114882373062, + 3635616438356, + 3635616438356, + 0, + 0, + 59742727807393, + 46558224181098, + ], + [ + 410303900576502, + 328874434203297, + 34015713376, + 2825380536, + 1254614658369, + 19178082192, + 19178082192, + 0, + 0, + 81676329409813, + 177306399289, + ], + [ + 165467232314162, + 143186965301687, + 90607346000, + 87511869764, + 1310711951430, + 71232876712, + 71232876712, + 0, + 0, + 26482194275958, + 4327139278949, + ], + [ + 539477602040470, + 460996451191946, + 9401630441, + 117225871288, + 1104099193874, + 189041095890, + 189041095890, + 0, + 0, + 98978041101744, + 26087529672756, + ], + [ + 531850598351846, + 491168165240726, + 59322952415, + 114998862808, + 853186193226, + 679452054795, + 679452054795, + 0, + 0, + 95125069871055, + 65591427432257, + ], + [ + 235795555335014, + 216254199337552, + 39970875010, + 72465733984, + 1320549227020, + 1306849315068, + 1306849315068, + 0, + 0, + 84755726923057, + 70894194588612, + ], + [ + 291867937678693, + 251676791784981, + 37847476295, + 12869574522, + 1365482717380, + 3106849315068, + 3106849315068, + 0, + 0, + 174955259443912, + 128888676444978, + ], + [ + 389353360076835, + 332644326504224, + 36006661725, + 8567817891, + 1446891789952, + 16438356164, + 16438356164, + 0, + 0, + 57973355104677, + 1210107074352, + ], + [ + 532819078834964, + 462584601169806, + 25881636087, + 38922737335, + 1002615725054, + 60273972603, + 60273972603, + 0, + 0, + 75907495719642, + 5991524744224, + ], + [ + 104776923491330, + 97617994554793, + 95616915764, + 36527873778, + 1076449684387, + 378082191781, + 378082191781, + 0, + 0, + 19538661623714, + 11496629813049, + ], + [ + 160149314509821, + 137147962161752, + 37552038419, + 25452599594, + 1388182916917, + 569863013699, + 569863013699, + 0, + 0, + 48867087654414, + 25811052610802, + ], + [ + 157922956553409, + 126738262468086, + 61800019020, + 39310078820, + 1114893290223, + 1967123287671, + 1967123287671, + 0, + 0, + 65657525063367, + 34902977942592, + ], + [ + 218878362767990, + 195605835968614, + 22452056064, + 119096114717, + 1207843181989, + 4315068493151, + 4315068493151, + 0, + 0, + 97711631051811, + 112807969571834, + ], + [ + 201184973855316, + 187015667083329, + 99251347914, + 80696237211, + 872649746795, + 10958904110, + 10958904110, + 0, + 0, + 14560754667954, + 386417278987, + ], + [ + 304239486109749, + 289233464605957, + 25168114967, + 20632123808, + 1262888041899, + 27397260274, + 27397260274, + 0, + 0, + 23036470205506, + 8021900108417, + ], + [ + 159207037708791, + 128310631052961, + 5638887972, + 60363702531, + 805117349968, + 375342465753, + 375342465753, + 0, + 0, + 34837632548724, + 5627077696764, + ], + [ + 325775941694713, + 261900607734613, + 35326993604, + 30665487357, + 1497824209010, + 873972602740, + 873972602740, + 0, + 0, + 130788707750069, + 68211070535875, + ], + [ + 244877757378729, + 201638897544950, + 115797560914, + 53500516578, + 936397286237, + 1583561643836, + 1583561643836, + 0, + 0, + 80430556162279, + 34042012037407, + ], + [ + 103379396387983, + 96057211675468, + 35766528149, + 13623051711, + 1195533168532, + 3602739726027, + 3602739726027, + 0, + 0, + 56819263246652, + 46658877308966, + ], + [ + 139619278993443, + 126166115130293, + 68390852542, + 2694639736, + 1481405184224, + 8219178082, + 8219178082, + 0, + 0, + 13942791817839, + 459507445263, + ], + [ + 254840084592337, + 212745465759875, + 60529183621, + 56464101889, + 1016943576358, + 79452054795, + 79452054795, + 0, + 0, + 44631191978079, + 2697566256781, + ], + [ + 333919527083839, + 290797136938707, + 56643618716, + 24085751924, + 1337763078851, + 194520547945, + 194520547945, + 0, + 0, + 67801894515280, + 24104024031499, + ], + [ + 250768871592747, + 200859417295781, + 60564134523, + 39982257030, + 1124764810225, + 895890410959, + 895890410959, + 0, + 0, + 82489777602739, + 33012799056959, + ], + [ + 215687758601963, + 191148567959323, + 102671435395, + 38176290572, + 945567444956, + 1460273972603, + 1460273972603, + 0, + 0, + 65859479247577, + 35713177477483, + ], + [ + 299450016830719, + 270135880039640, + 91233398269, + 103597722286, + 1257933520553, + 2427397260274, + 2427397260274, + 0, + 0, + 120743701281322, + 100818143680892, + ], + [ + 556775889052005, + 469745887243504, + 62575943724, + 50371996609, + 898721986627, + 5479452055, + 5479452055, + 0, + 0, + 87018795608235, + 19597917, + ], + [ + 331865926950003, + 293466898690972, + 95587099600, + 32355562773, + 1156334955915, + 30136986301, + 30136986301, + 0, + 0, + 41247169132505, + 2643104084796, + ], + [ + 390639510305029, + 368128655624572, + 28216250127, + 36522046291, + 899754605612, + 372602739726, + 372602739726, + 0, + 0, + 59435174666190, + 37757276948471, + ], + [ + 326219228835100, + 267244852463076, + 61023181796, + 95676762206, + 991987938358, + 906849315068, + 906849315068, + 0, + 0, + 90451911588069, + 39451906218187, + ], + [ + 380541902037525, + 325358539045564, + 110706518226, + 8520844028, + 1197970222764, + 1654794520548, + 1654794520548, + 0, + 0, + 158191013116625, + 83880798980471, + ], + [ + 447859662976959, + 392619592669039, + 61327992559, + 71744063213, + 891646161264, + 2413698630137, + 2413698630137, + 0, + 0, + 142695485449837, + 99870844322797, + ], + [ + 513635236145054, + 450323832415932, + 92769071540, + 3108582229, + 888199326003, + 19178082192, + 19178082192, + 0, + 0, + 64055042004270, + 415107668561, + ], + [ + 515372366992160, + 412766462795377, + 25637422896, + 32096661985, + 967260407866, + 30136986301, + 30136986301, + 0, + 0, + 102646813931874, + 170275708983, + ], + [ + 51196520747050, + 41205450566278, + 41460333480, + 4295159135, + 1401838906800, + 490410958904, + 490410958904, + 0, + 0, + 16442872111879, + 6192936230568, + ], + [ + 171360747547500, + 156623213164619, + 97981949231, + 15936391859, + 1256453301809, + 632876712329, + 632876712329, + 0, + 0, + 46882420948217, + 28776258479082, + ], + [ + 106286037511500, + 89096868302301, + 39410889778, + 49613162910, + 1302718296431, + 1605479452055, + 1605479452055, + 0, + 0, + 45033150516481, + 29710548500405, + ], + [ + 86231397922992, + 71069797023617, + 9000358394, + 72990402122, + 1300095701750, + 2797260273973, + 2797260273973, + 0, + 0, + 43433605452646, + 35745020745336, + ], + [ + 333656305441956, + 282271874728282, + 48645440233, + 32360840411, + 1107424662136, + 16438356164, + 16438356164, + 0, + 0, + 51579659502017, + 191675123702, + ], + [ + 518387036372390, + 493320732639026, + 14986435245, + 91830946270, + 960176642364, + 32876712329, + 32876712329, + 0, + 0, + 34761954385629, + 10361949689080, + ], + [ + 424090780518840, + 365589787338844, + 115255413298, + 95775659074, + 903360555495, + 189041095890, + 189041095890, + 0, + 0, + 71453135176609, + 13448015632165, + ], + [ + 440700885134897, + 353706775446452, + 39879755061, + 101879396989, + 1466844193553, + 597260273973, + 597260273973, + 0, + 0, + 145980698422942, + 68904249184620, + ], + [ + 254001172487309, + 212997502400276, + 8729333847, + 101847876640, + 1045808378289, + 1405479452055, + 1405479452055, + 0, + 0, + 78276442429566, + 53498007053373, + ], + [ + 231760842030220, + 214893290980336, + 38174491039, + 88460121605, + 894404744987, + 2684931506849, + 2684931506849, + 0, + 0, + 69195004909564, + 67477293881540, + ], + [ + 271689459075241, + 217614539290818, + 91098113899, + 29706753455, + 1013447919508, + 13698630137, + 13698630137, + 0, + 0, + 54124221961876, + 2609347146, + ], + [ + 168862273630684, + 137114512663515, + 93405016006, + 79652295029, + 873247842620, + 52054794521, + 52054794521, + 0, + 0, + 31896246443287, + 242309091482, + ], + [ + 523311584031678, + 453167644613390, + 48177855695, + 9480351935, + 1455140491273, + 153424657534, + 153424657534, + 0, + 0, + 105990300316269, + 34817824522150, + ], + [ + 183982228078649, + 151235311932503, + 25946342745, + 67635284544, + 1164439170994, + 813698630137, + 813698630137, + 0, + 0, + 56411553447313, + 27369854668653, + ], + [ + 141025335987207, + 133738468332402, + 30144853, + 106309720425, + 983873546948, + 1126027397260, + 1126027397260, + 0, + 0, + 32122344403080, + 32947173776590, + ], + [ + 47390883278420, + 43579853250274, + 42607222795, + 15093617874, + 1336734260609, + 4172602739726, + 4172602739726, + 0, + 0, + 30078487337664, + 24521235908974, + ], + [ + 519746814131825, + 465470378731592, + 86652203457, + 105600780502, + 1224116637480, + 16438356164, + 16438356164, + 0, + 0, + 56543541221350, + 2425185869047, + ], + [ + 159090901035134, + 132860273588220, + 117344166325, + 79225569203, + 974483541184, + 21917808219, + 21917808219, + 0, + 0, + 26294567269962, + 65010318320, + ], + [ + 317286546613153, + 255140594304966, + 38541659125, + 83394872577, + 920153429786, + 106849315068, + 106849315068, + 0, + 0, + 63687398040398, + 2552816531112, + ], + [ + 429691675899638, + 378222757596601, + 99089230103, + 98618083930, + 1259612161501, + 928767123288, + 928767123288, + 0, + 0, + 132052286248457, + 85022907699102, + ], + [ + 439653988541976, + 412525479805582, + 102687776562, + 91691161836, + 1368716620729, + 1128767123288, + 1128767123288, + 0, + 0, + 148208076465604, + 121608572850147, + ], + [ + 588124986200468, + 480519108734579, + 116625750643, + 4809694477, + 1091303149517, + 3230136986301, + 3230136986301, + 0, + 0, + 291160364769487, + 134832909486357, + ], + [ + 333568309512802, + 305418547052249, + 5960461812, + 108391935596, + 1016537050133, + 16438356164, + 16438356164, + 0, + 0, + 29327731011008, + 1461373481342, + ], + [ + 25912882497079, + 24122751982769, + 36635954891, + 12519592160, + 855091314806, + 73972602740, + 73972602740, + 0, + 0, + 2432813351984, + 624462821629, + ], + [ + 433838863224893, + 377760420658897, + 82260230835, + 30235031247, + 1012592189169, + 112328767123, + 112328767123, + 0, + 0, + 67770163496396, + 10949057768522, + ], + [ + 204452065989657, + 167948297254261, + 8485861904, + 38422599924, + 1088700765655, + 863013698630, + 863013698630, + 0, + 0, + 62541999605841, + 28903974441643, + ], + [ + 169488877975627, + 141288979953422, + 25454405805, + 115002587830, + 1149133258496, + 1726027397260, + 1726027397260, + 0, + 0, + 59456629991098, + 44382551245367, + ], + [ + 23642331837205, + 21690091282835, + 103298782967, + 41884266353, + 1216897106238, + 4252054794521, + 4252054794521, + 0, + 0, + 11907964204257, + 8474932195337, + ], + [ + 239887702760213, + 222815401353586, + 87278792995, + 23448993279, + 801819138548, + 2739726027, + 2739726027, + 0, + 0, + 17090982154900, + 1791060545, + ], + [ + 200179917448821, + 179337483296617, + 73751232495, + 85974396769, + 806970427425, + 49315068493, + 49315068493, + 0, + 0, + 22161243159408, + 1454577968093, + ], + [ + 613171855692006, + 499768846581144, + 39147173919, + 34030176177, + 955281473982, + 320547945205, + 320547945205, + 0, + 0, + 139191235465001, + 26705501761933, + ], + [ + 400239950209325, + 372178970023914, + 84553904972, + 88791324901, + 1071612364591, + 509589041096, + 509589041096, + 0, + 0, + 80212608029058, + 53748639815192, + ], + [ + 90928376834844, + 84281386390018, + 76967646143, + 52803744620, + 1498467904127, + 1306849315068, + 1306849315068, + 0, + 0, + 37602198372519, + 30279185824691, + ], + [ + 541526214652867, + 469845767576937, + 9604527472, + 88598695761, + 942183291680, + 3408219178082, + 3408219178082, + 0, + 0, + 193389088771264, + 188639682553159, + ], + [ + 455694116485296, + 366956293748850, + 35911838082, + 98678391110, + 1443148599900, + 19178082192, + 19178082192, + 0, + 0, + 88967513132772, + 564755213616, + ], + [ + 89396145018084, + 73137311040150, + 32310400380, + 55292703067, + 963770745634, + 46575342466, + 46575342466, + 0, + 0, + 16379920870513, + 193292104468, + ], + [ + 135009476724797, + 113519106136048, + 60629567766, + 87410821475, + 941730664577, + 369863013699, + 369863013699, + 0, + 0, + 28264089358800, + 7901949286667, + ], + [ + 487551735040821, + 433312323492626, + 77176739338, + 55287891120, + 830513454514, + 967123287671, + 967123287671, + 0, + 0, + 112911136215868, + 57749364863153, + ], + [ + 426177781116695, + 369958079834913, + 99198675651, + 57939042592, + 1220828908897, + 1542465753425, + 1542465753425, + 0, + 0, + 161218201834441, + 101086823744695, + ], + [ + 41227316861414, + 35091394311716, + 79014590963, + 22174873201, + 1067006137544, + 4789041095890, + 4789041095890, + 0, + 0, + 21418044159182, + 12997991123286, + ], + [ + 374432183357241, + 352175022603011, + 62835247675, + 73758379608, + 839627897915, + 5479452055, + 5479452055, + 0, + 0, + 22472644976905, + 234347426836, + ], + [ + 126701213104947, + 102072077075540, + 92111782329, + 35473758629, + 834776154949, + 46575342466, + 46575342466, + 0, + 0, + 24769242920316, + 78997141070, + ], + [ + 451295931675355, + 387694169362084, + 40963728399, + 23503835417, + 867641284560, + 484931506849, + 484931506849, + 0, + 0, + 95576252887862, + 31347200506912, + ], + [ + 137166992526889, + 122814042869478, + 65438029127, + 116522058719, + 990908557269, + 616438356164, + 616438356164, + 0, + 0, + 28805667883974, + 17072888960389, + ], + [ + 241921552129216, + 206490836124842, + 63070108826, + 73335389179, + 1266252217582, + 1830136986301, + 1830136986301, + 0, + 0, + 99233616446767, + 69677478163547, + ], + [ + 430095534707821, + 376079735505784, + 69534575715, + 10789052731, + 1190923997079, + 3983561643836, + 3983561643836, + 0, + 0, + 242189659787092, + 159935085298739, + ], + [ + 388375251270024, + 356776071222520, + 28867472386, + 54054126284, + 1088976757579, + 2739726027, + 2739726027, + 0, + 0, + 31602548364731, + 19265823005, + ], + [ + 442910063306873, + 409246351210667, + 52865372540, + 5515723355, + 1365013346298, + 35616438356, + 35616438356, + 0, + 0, + 45818102724428, + 11844741001832, + ], + [ + 385257640697097, + 334670170966061, + 24241126256, + 86223114471, + 1213208011964, + 164383561644, + 164383561644, + 0, + 0, + 69392624438481, + 20954569407628, + ], + [ + 473273139530759, + 445124037658822, + 109585566526, + 82492348288, + 966274617769, + 920547945205, + 920547945205, + 0, + 0, + 107768660844376, + 76940505204463, + ], + [ + 81849505217679, + 75399003299291, + 71817762170, + 93592380439, + 971535655609, + 1608219178082, + 1608219178082, + 0, + 0, + 22718485089239, + 18233614245048, + ], + [ + 514962721627736, + 475173864417115, + 5836135575, + 12447414496, + 933498153254, + 3947945205479, + 3947945205479, + 0, + 0, + 237414426284005, + 205042522323258, + ], + [ + 259913378263039, + 243399204536210, + 115623388842, + 13548736684, + 897149426696, + 16438356164, + 16438356164, + 0, + 0, + 18089795112493, + 1389212199136, + ], + [ + 121643324399298, + 113530741909606, + 117503225704, + 113584084382, + 1291418312481, + 65753424658, + 65753424658, + 0, + 0, + 13530907356348, + 5465208842280, + ], + [ + 82532421146496, + 66769564415931, + 38017276926, + 1591037027, + 856407028760, + 306849315068, + 306849315068, + 0, + 0, + 18534487422313, + 2496843642134, + ], + [ + 614047507742932, + 499194290912980, + 40139848267, + 3574028234, + 1138995076957, + 591780821918, + 591780821918, + 0, + 0, + 181674073876510, + 62982163552027, + ], + [ + 50849230851845, + 43335025816773, + 11239470248, + 15649146489, + 1097196671705, + 1158904109589, + 1158904109589, + 0, + 0, + 17027474878142, + 9738541340498, + ], + [ + 389518657807624, + 358797383642474, + 27227443572, + 57127693429, + 1255669364660, + 2564383561644, + 2564383561644, + 0, + 0, + 178729674157522, + 163657232936466, + ], + [ + 179310689224174, + 164220375451448, + 64541003566, + 106601481860, + 1151248786507, + 16438356164, + 16438356164, + 0, + 0, + 16168967285149, + 1156563225285, + ], + [ + 256268051114008, + 222668046676742, + 69043084360, + 101126057892, + 1441989458390, + 46575342466, + 46575342466, + 0, + 0, + 38679829706464, + 5378457234281, + ], + [ + 346489241993648, + 297969091163709, + 101001766797, + 105330364244, + 1474083882212, + 369863013699, + 369863013699, + 0, + 0, + 91182318924521, + 44708338302109, + ], + [ + 542214781074316, + 465517826824446, + 69679898095, + 83925846193, + 1272769364516, + 569863013699, + 569863013699, + 0, + 0, + 148807210263211, + 77205586498968, + ], + [ + 385580359953294, + 348283668962207, + 23392551005, + 95724943257, + 917470465405, + 1989041095890, + 1989041095890, + 0, + 0, + 108565042250549, + 98214746026212, + ], + [ + 137606094090910, + 113924940960185, + 80197942836, + 119283661439, + 803882128093, + 2367123287671, + 2367123287671, + 0, + 0, + 37500939195672, + 23020771557233, + ], + [ + 148247992863768, + 128443893000984, + 44435022167, + 56980980939, + 1118789061040, + 10958904110, + 10958904110, + 0, + 0, + 19841601763194, + 57329217197, + ], + [ + 22355605202348, + 20394062224545, + 54303097132, + 35644377297, + 944235487354, + 41095890411, + 41095890411, + 0, + 0, + 2232859835740, + 267135241118, + ], + [ + 361239617883648, + 297072750858640, + 92303982462, + 106420110211, + 848214968321, + 117808219178, + 117808219178, + 0, + 0, + 66528615866747, + 3352686830542, + ], + [ + 484387215761735, + 439668388825263, + 5670696756, + 45411729788, + 1394576656168, + 860273972603, + 860273972603, + 0, + 0, + 160001926975052, + 123647480786402, + ], + [ + 552736170753922, + 482040071104990, + 45742246915, + 4449961100, + 888919562743, + 1063013698630, + 1063013698630, + 0, + 0, + 152115536544678, + 73048609641350, + ], + [ + 141205910867378, + 119623959252663, + 7841620385, + 69512542221, + 1378327112308, + 4358904109589, + 4358904109589, + 0, + 0, + 86114252416173, + 82060335575290, + ], + [ + 86314561783560, + 77345777517218, + 57920212905, + 94945366378, + 1487428428957, + 10958904110, + 10958904110, + 0, + 0, + 9343379845745, + 397784446424, + ], + [ + 473509196595326, + 393613140197310, + 46299383453, + 16650010350, + 1299721440004, + 27397260274, + 27397260274, + 0, + 0, + 81600751265219, + 1613850140847, + ], + [ + 346050650353284, + 314946784392449, + 85480772505, + 40335559977, + 956793964097, + 131506849315, + 131506849315, + 0, + 0, + 45134126577692, + 13360185630705, + ], + [ + 223888214614678, + 188602045718416, + 63028544925, + 32024452322, + 1403392407303, + 545205479452, + 545205479452, + 0, + 0, + 69169887151030, + 33236993592961, + ], + [ + 358777916180977, + 327257453986612, + 83671853516, + 53081736249, + 1340658598750, + 1339726027397, + 1339726027397, + 0, + 0, + 136066698874720, + 101225863565136, + ], + [ + 512582449856259, + 473333224769909, + 77408864238, + 56158902843, + 912751526314, + 4517808219178, + 4517808219178, + 0, + 0, + 197560775750355, + 151982344328899, + ], + [ + 95640204854662, + 85730055213226, + 43028982451, + 44626050453, + 1137334791631, + 2739726027, + 2739726027, + 0, + 0, + 9909388020084, + 615808148, + ], + [ + 51489812753233, + 44102342118633, + 43059297384, + 104832496712, + 889526725232, + 41095890411, + 41095890411, + 0, + 0, + 7463155598729, + 153931908983, + ], + [ + 469052814333542, + 418502487092300, + 102293050248, + 113029279329, + 826540600236, + 383561643836, + 383561643836, + 0, + 0, + 77917236716894, + 30239229178626, + ], + [ + 314305125534896, + 271369976087481, + 63010330233, + 117692778693, + 1052321803344, + 975342465753, + 975342465753, + 0, + 0, + 83967228214506, + 51335002984437, + ], + [ + 82660328785208, + 75694801156986, + 60148346565, + 88564822803, + 1025621576230, + 1997260273973, + 1997260273973, + 0, + 0, + 26303675281902, + 22167992651358, + ], + [ + 396935340018401, + 375563481298031, + 110332846959, + 34624132932, + 866786598283, + 2460273972603, + 2460273972603, + 0, + 0, + 128214255163848, + 81909531693273, + ], + [ + 491789019368961, + 455253562542515, + 112153422926, + 85911265535, + 1029915897193, + 16438356164, + 16438356164, + 0, + 0, + 39492765875238, + 2918711107082, + ], + [ + 239759879844437, + 198790040886690, + 26305961369, + 76430047373, + 1413862219166, + 21917808219, + 21917808219, + 0, + 0, + 41514094531712, + 699447917035, + ], + [ + 541008054220179, + 484324850633908, + 92420501189, + 101224230187, + 978442939421, + 235616438356, + 235616438356, + 0, + 0, + 86870553293877, + 31956970555896, + ], + [ + 27073288518378, + 22972814334624, + 109766214854, + 57180441873, + 1193456008889, + 572602739726, + 572602739726, + 0, + 0, + 7441335692083, + 3204014585273, + ], + [ + 478093727069199, + 438376981104977, + 101346810161, + 51450917589, + 1293029353206, + 1060273972603, + 1060273972603, + 0, + 0, + 159451732349997, + 112220579749275, + ], + [ + 423622033484966, + 403047962786169, + 102528676369, + 26562217538, + 1266031634936, + 2290410958904, + 2290410958904, + 0, + 0, + 190417300233050, + 143242556339872, + ], + [ + 382645340085326, + 318382301112427, + 112952320548, + 116116994481, + 824953656311, + 19178082192, + 19178082192, + 0, + 0, + 64130752261631, + 18356254756, + ], + [ + 143607268408218, + 132513813197340, + 109860018103, + 1240367633, + 999944842840, + 46575342466, + 46575342466, + 0, + 0, + 14002234924479, + 2603386704927, + ], + [ + 90186201111932, + 84611892789025, + 3668794556, + 2333505831, + 1390482362322, + 169863013699, + 169863013699, + 0, + 0, + 14633639898031, + 9052582114911, + ], + [ + 444531852928001, + 368402024947248, + 58646035112, + 52765428663, + 843994570355, + 871232876712, + 871232876712, + 0, + 0, + 113849962406164, + 40428356190734, + ], + [ + 534981809265785, + 491260996935203, + 59655700833, + 68779799488, + 1121487883532, + 1315068493151, + 1315068493151, + 0, + 0, + 165374354400057, + 127907975229560, + ], + [ + 585620094786763, + 472023845523093, + 86281513817, + 54295629635, + 1119897756254, + 4383561643836, + 4383561643836, + 0, + 0, + 282113574695020, + 174802868313546, + ], + [ + 291036360476876, + 268075539105870, + 101110077258, + 44009452142, + 1179269448547, + 10958904110, + 10958904110, + 0, + 0, + 24245708309904, + 1219337087107, + ], + [ + 475831121917250, + 444033292331389, + 78318709495, + 95860665028, + 959769832053, + 30136986301, + 30136986301, + 0, + 0, + 37738717616841, + 6141309833155, + ], + [ + 168257577363904, + 156903239337888, + 103747235791, + 110238102111, + 1491952296815, + 312328767123, + 312328767123, + 0, + 0, + 36481183828559, + 25653869776938, + ], + [ + 391413585381901, + 365219756303571, + 24315193930, + 15519117094, + 1044493986005, + 942465753425, + 942465753425, + 0, + 0, + 103239906488220, + 76049472186203, + ], + [ + 83757597914219, + 69408763461423, + 12137526499, + 49433022413, + 1007667850680, + 1605479452055, + 1605479452055, + 0, + 0, + 28770639115073, + 17109542221526, + ], + [ + 418798013682558, + 394157676678654, + 33648778384, + 53787249929, + 1137323286124, + 2153424657534, + 2153424657534, + 0, + 0, + 160157695265354, + 145565073402725, + ], + [ + 60109623745693, + 51944268973813, + 40159254868, + 108430993695, + 1179265559209, + 16438356164, + 16438356164, + 0, + 0, + 8223351706945, + 97080435779, + ], + [ + 319624186902631, + 286534068948985, + 21370326193, + 103223557443, + 1328001554303, + 52054794521, + 52054794521, + 0, + 0, + 41142658958671, + 8768544679007, + ], + [ + 126152827170539, + 113766923338686, + 85521082145, + 23677718059, + 1386836297189, + 413698630137, + 413698630137, + 0, + 0, + 31870825022612, + 18344420748884, + ], + [ + 161301325680853, + 152556531048443, + 94915968482, + 102052492435, + 1087246363847, + 506849315068, + 506849315068, + 0, + 0, + 31276203225451, + 23219832820144, + ], + [ + 145295582108333, + 127676851036108, + 83451221435, + 88879636418, + 1385768100394, + 1926027397260, + 1926027397260, + 0, + 0, + 62933405358264, + 48575187215574, + ], + [ + 44946628589621, + 37135633713071, + 105924837856, + 78970527579, + 1074690279781, + 2953424657534, + 2953424657534, + 0, + 0, + 17928032991759, + 10871465754589, + ], + [ + 437941158360686, + 401758285862653, + 54054274660, + 21819171991, + 1298181748416, + 13698630137, + 13698630137, + 0, + 0, + 39409044860604, + 3156319791013, + ], + [ + 525100637526983, + 436400280728798, + 68282889045, + 50038119843, + 1460945758446, + 76712328767, + 76712328767, + 0, + 0, + 102270344488373, + 13667681872770, + ], + [ + 473660056658923, + 442040133699624, + 111413572179, + 754553892, + 1263588916721, + 356164383562, + 356164383562, + 0, + 0, + 100055778416025, + 60575991832273, + ], + [ + 85101801096316, + 78317010452998, + 73890067266, + 5549013713, + 924942915300, + 775342465753, + 775342465753, + 0, + 0, + 19698807256269, + 11124548647007, + ], + [ + 584013587916821, + 491027889698930, + 23244424479, + 12205291979, + 1061019394961, + 1164383561644, + 1164383561644, + 0, + 0, + 195090703194428, + 100919079521653, + ], + [ + 233130182168000, + 202589344719450, + 7702045955, + 49039284151, + 822314650208, + 4693150684932, + 4693150684932, + 0, + 0, + 91008661096589, + 82018467520283, + ], + [ + 487611079595735, + 426206910600965, + 108604896557, + 20345596740, + 1337551225110, + 8219178082, + 8219178082, + 0, + 0, + 61855393725576, + 329266195565, + ], + [ + 348385970985172, + 299377273841148, + 1427912106, + 13360943065, + 1480023512744, + 52054794521, + 52054794521, + 0, + 0, + 56853219140885, + 7956337933791, + ], + [ + 240720136239493, + 222676681789856, + 18919068715, + 9132248731, + 1267971646895, + 210958904110, + 210958904110, + 0, + 0, + 41204997772486, + 22985735833563, + ], + [ + 27868748599578, + 26454201491181, + 90689042548, + 85967691323, + 1246614111387, + 643835616438, + 643835616438, + 0, + 0, + 6779455138345, + 5405141277822, + ], + [ + 579417047485425, + 496445344763859, + 73871613547, + 19094456804, + 1372705837472, + 1383561643836, + 1383561643836, + 0, + 0, + 248412391597801, + 153168393887033, + ], + [ + 312057646949991, + 251485961913104, + 37246927289, + 90378754212, + 1423666437955, + 2810958904110, + 2810958904110, + 0, + 0, + 163528299055152, + 128957492023645, + ], + [ + 387123561153836, + 316959615917062, + 68931740073, + 106518911395, + 1309027553581, + 5479452055, + 5479452055, + 0, + 0, + 70098461850439, + 860737537, + ], + [ + 288426359444051, + 251447526746608, + 36018111507, + 34868883615, + 1018311417624, + 54794520548, + 54794520548, + 0, + 0, + 40029930039529, + 3114943026939, + ], + [ + 487984026392566, + 452950136115206, + 79251394957, + 72642991626, + 1472061576318, + 197260273973, + 197260273973, + 0, + 0, + 89333349850853, + 54529626403164, + ], + [ + 266814035978951, + 253114915689223, + 104282950933, + 11581680297, + 1192141621276, + 638356164384, + 638356164384, + 0, + 0, + 66638941575607, + 46288075866731, + ], + [ + 172044695071581, + 151055049248446, + 10762591787, + 18456938039, + 1474396431761, + 1868493150685, + 1868493150685, + 0, + 0, + 89476409262915, + 70110964782067, + ], + [ + 385000408712695, + 327904410606983, + 52296206869, + 87872511739, + 1120144945658, + 4660273972603, + 4660273972603, + 0, + 0, + 172703559839696, + 151642546859793, + ], + [ + 329213838153825, + 303618655015822, + 80434570418, + 52873508791, + 1128092996574, + 19178082192, + 19178082192, + 0, + 0, + 28507572471215, + 2864954336616, + ], + [ + 323798118381305, + 272170583356474, + 15934798933, + 63599604050, + 1241938460084, + 54794520548, + 54794520548, + 0, + 0, + 55124798112691, + 3964427989989, + ], + [ + 143421509796390, + 126323564663481, + 78129651804, + 3174260263, + 1173455640425, + 378082191781, + 378082191781, + 0, + 0, + 32799866702531, + 14207747535180, + ], + [ + 185468754733851, + 149884416957377, + 115331517542, + 37399608157, + 1296189887617, + 819178082192, + 819178082192, + 0, + 0, + 65132918771155, + 27253620010179, + ], + [ + 191333207224678, + 172739767450342, + 75229491063, + 44186543207, + 1034231324396, + 1435616438356, + 1435616438356, + 0, + 0, + 60252912761668, + 39678456997252, + ], + [ + 323510865459086, + 301257940404442, + 14371754567, + 96979164797, + 1492785354185, + 2901369863014, + 2901369863014, + 0, + 0, + 172818190696209, + 185858440528832, + ], + [ + 193256049155744, + 175646114086077, + 74060668094, + 12235197624, + 1060550000025, + 19178082192, + 19178082192, + 0, + 0, + 18703497195190, + 1004102613027, + ], + [ + 319090652849667, + 263229906348020, + 84777056981, + 30933674351, + 1067840129815, + 41095890411, + 41095890411, + 0, + 0, + 56973307243103, + 954775905817, + ], + [ + 136685138570850, + 115500558325693, + 114879805916, + 93270056308, + 1338778498005, + 490410958904, + 490410958904, + 0, + 0, + 38031330875689, + 17320236898393, + ], + [ + 521205356460522, + 474479584026880, + 12258092472, + 112959528469, + 1207042281564, + 838356164384, + 838356164384, + 0, + 0, + 138399866228738, + 113327827023208, + ], + [ + 215427058543017, + 199089339844238, + 20219662464, + 93056430660, + 937241148519, + 1350684931507, + 1350684931507, + 0, + 0, + 53334856883240, + 47418623706578, + ], + [ + 75548680442014, + 60710679375015, + 7187935315, + 70187680763, + 896167050355, + 3227397260274, + 3227397260274, + 0, + 0, + 28239922428390, + 20762756824014, + ], + [ + 480501430516569, + 385792702607558, + 50455461701, + 36772377312, + 977384205458, + 5479452055, + 5479452055, + 0, + 0, + 94700556267603, + 237172, + ], + [ + 218528370498975, + 182342996083965, + 69382151168, + 11568948682, + 1448131906360, + 76712328767, + 76712328767, + 0, + 0, + 42169701102237, + 5693684399394, + ], + [ + 409828156136058, + 333771691591715, + 63668328405, + 104432311592, + 1437914798052, + 109589041096, + 109589041096, + 0, + 0, + 88300471812169, + 13680525583578, + ], + [ + 261147981810981, + 219457226745344, + 24843483284, + 27386996574, + 902165145198, + 827397260274, + 827397260274, + 0, + 0, + 68320651192854, + 27747132724832, + ], + [ + 434114035442908, + 364292057255152, + 68607525499, + 57749276872, + 1474061164093, + 1742465753425, + 1742465753425, + 0, + 0, + 207550286542275, + 141928541444015, + ], + [ + 251428411117781, + 202709612234181, + 22378162625, + 43976012438, + 932291488917, + 3690410958904, + 3690410958904, + 0, + 0, + 108870508604692, + 72998960736996, + ], + [ + 581576368356940, + 485003162567240, + 81591949545, + 79647552973, + 1057626584341, + 2739726027, + 2739726027, + 0, + 0, + 96553169002993, + 18945, + ], + [ + 302411052468470, + 262948652744283, + 22789884331, + 33612562471, + 1270310535893, + 52054794521, + 52054794521, + 0, + 0, + 44698360667128, + 5367816357754, + ], + [ + 448792507636339, + 410521844936025, + 59971297263, + 113340207337, + 931415365683, + 136986301370, + 136986301370, + 0, + 0, + 55057717566563, + 18723308202467, + ], + [ + 445065780798131, + 415136899486388, + 112681341541, + 115389139530, + 1384771198885, + 786301369863, + 786301369863, + 0, + 0, + 129594564443896, + 102636669892223, + ], + [ + 272583995980073, + 250797047568989, + 111036652442, + 54112267063, + 1139842399606, + 1575342465753, + 1575342465753, + 0, + 0, + 93938212399369, + 65073154312089, + ], + [ + 174407867979103, + 153890322655476, + 96924022890, + 57164629664, + 1262294920854, + 3457534246575, + 3457534246575, + 0, + 0, + 85313159111926, + 61656750530049, + ], + [ + 467979579458613, + 405132799897360, + 108958718537, + 54588547290, + 1198536013060, + 16438356164, + 16438356164, + 0, + 0, + 63780944538202, + 837811432916, + ], + [ + 162285272942377, + 144009091460041, + 102203313295, + 37922720014, + 1176191848774, + 32876712329, + 32876712329, + 0, + 0, + 20021547057920, + 1635645712507, + ], + [ + 461778658699845, + 393141727092169, + 13754755919, + 9956142911, + 1144973940035, + 101369863014, + 101369863014, + 0, + 0, + 80178708792342, + 11548617296479, + ], + [ + 236677848425422, + 203602102608316, + 67091732005, + 62369184775, + 1325320592928, + 882191780822, + 882191780822, + 0, + 0, + 78995422596064, + 47355212925203, + ], + [ + 404720840872097, + 338392589835199, + 74946040569, + 11720085373, + 1132584703974, + 1660273972603, + 1660273972603, + 0, + 0, + 163499328817559, + 85500724429010, + ], + [ + 208488970444419, + 191993629336641, + 24535238227, + 54248750634, + 922618956197, + 2619178082192, + 2619178082192, + 0, + 0, + 70405557451169, + 62351196468969, + ], + [ + 249234137902438, + 206983325127822, + 75289315898, + 34693970017, + 1450183140054, + 16438356164, + 16438356164, + 0, + 0, + 42694778369641, + 413149055327, + ], + [ + 431197228310380, + 359744088701523, + 111082716249, + 93491559323, + 927735178604, + 73972602740, + 73972602740, + 0, + 0, + 74062028501985, + 2915243696562, + ], + [ + 593021673413955, + 495990180585262, + 23698401658, + 111176393476, + 1267065195991, + 312328767123, + 312328767123, + 0, + 0, + 139334728345111, + 50987538574338, + ], + [ + 403294379598030, + 372615130169034, + 47709785558, + 56397907401, + 1476159831741, + 698630136986, + 698630136986, + 0, + 0, + 125436866228911, + 96944787084061, + ], + [ + 526621115171658, + 477328796332532, + 91539604836, + 91076845376, + 954866909176, + 1547945205479, + 1547945205479, + 0, + 0, + 146254627230461, + 103310922566357, + ], + [ + 45623084920313, + 37153625739296, + 108357073353, + 9493804139, + 1312681008957, + 4213698630137, + 4213698630137, + 0, + 0, + 27112972078014, + 14797826939660, + ], + [ + 206976957690940, + 176213808033000, + 81372466416, + 119475541342, + 1339650789597, + 13698630137, + 13698630137, + 0, + 0, + 30938774917121, + 263844844156, + ], + [ + 285199944226877, + 232235518032775, + 106111952327, + 92623460873, + 1313962012152, + 30136986301, + 30136986301, + 0, + 0, + 53720050200914, + 866938122996, + ], + [ + 134427603601860, + 107584594602874, + 12798954374, + 60284162614, + 954238598295, + 104109589041, + 104109589041, + 0, + 0, + 27589471836147, + 1113501086763, + ], + [ + 320480053595399, + 287238118275106, + 65224734356, + 38204554679, + 1026323896788, + 893150684932, + 893150684932, + 0, + 0, + 85189055381195, + 50150548905893, + ], + [ + 347733976902903, + 303873302569195, + 99051464218, + 80335956189, + 1404956527068, + 1353424657534, + 1353424657534, + 0, + 0, + 137499793358328, + 95257690548808, + ], + [ + 438420786612478, + 377587263080324, + 33367374925, + 79403681185, + 1152704787085, + 3950684931507, + 3950684931507, + 0, + 0, + 205252022535175, + 184851003502180, + ], + [ + 196743288088860, + 158012501583469, + 92771460840, + 87323150493, + 1192409120401, + 8219178082, + 8219178082, + 0, + 0, + 38706282497357, + 615307513, + ], + [ + 148344942734972, + 139607956460852, + 99990132173, + 118509924767, + 1285071938902, + 76712328767, + 76712328767, + 0, + 0, + 16520777463011, + 7955073526116, + ], + [ + 578980423255712, + 489781442921117, + 29101651602, + 84448479004, + 825522838260, + 169863013699, + 169863013699, + 0, + 0, + 98014864403083, + 11955452502123, + ], + [ + 132194187856145, + 108722305891209, + 113889473091, + 42532203576, + 1285672292698, + 717808219178, + 717808219178, + 0, + 0, + 43261607038539, + 18458487734365, + ], + [ + 34987529888522, + 31895403862425, + 86087173468, + 34576089279, + 1036429319738, + 1917808219178, + 1917808219178, + 0, + 0, + 12421567212391, + 8285400086726, + ], + [ + 47395735524789, + 42848819730227, + 116000975429, + 1984820644, + 1194516456195, + 2600000000000, + 2600000000000, + 0, + 0, + 22700234231973, + 13587387780265, + ], + [ + 318759443111014, + 260203052705261, + 94963789640, + 109478204852, + 1074091955791, + 19178082192, + 19178082192, + 0, + 0, + 58481326845109, + 75763543684, + ], + [ + 501051791236098, + 436354203544051, + 21885758861, + 87940986043, + 1129297723002, + 27397260274, + 27397260274, + 0, + 0, + 66645978251672, + 2440012047711, + ], + [ + 529411672681474, + 467594848807965, + 11824456730, + 45288711727, + 1394502839824, + 443835616438, + 443835616438, + 0, + 0, + 139094160372919, + 81492615365639, + ], + [ + 395122438861794, + 365970236015262, + 65982519970, + 73921953992, + 1094628138498, + 884931506849, + 884931506849, + 0, + 0, + 101015129996447, + 74822631117790, + ], + [ + 67309465713172, + 56691240694690, + 53764971471, + 99075673608, + 1282429363670, + 1912328767123, + 1912328767123, + 0, + 0, + 27423396647806, + 20399489031014, + ], + [ + 46695139158516, + 44037787233205, + 9024065833, + 109963356643, + 1131938645967, + 4284931506849, + 4284931506849, + 0, + 0, + 19759486761176, + 25656720893257, + ], + [ + 120189731210229, + 107012862747263, + 82019189843, + 119579053925, + 1266245205995, + 16438356164, + 16438356164, + 0, + 0, + 13676970346599, + 554902066507, + ], + [ + 157219188200808, + 128399469054736, + 40104768424, + 32099934409, + 826145074063, + 76712328767, + 76712328767, + 0, + 0, + 29289754031595, + 510430049631, + ], + [ + 314538935960804, + 285936201025066, + 58312555778, + 108405365092, + 1486774403049, + 372602739726, + 372602739726, + 0, + 0, + 75672557439415, + 50539095866330, + ], + [ + 369884708989183, + 330944540022200, + 32523950345, + 21416711497, + 1173390454782, + 950684931507, + 950684931507, + 0, + 0, + 114084766509295, + 74430086224438, + ], + [ + 73586949546094, + 65861336745869, + 48948665823, + 56940339613, + 935063166407, + 1410958904110, + 1410958904110, + 0, + 0, + 20734269361328, + 13909957000847, + ], + [ + 128199705266363, + 103437735644972, + 90454046666, + 12754262170, + 953779460678, + 2824657534247, + 2824657534247, + 0, + 0, + 55469522170774, + 24551483704111, + ], + [ + 404946397010646, + 335817658462538, + 9791305842, + 10349174298, + 1131377129965, + 8219178082, + 8219178082, + 0, + 0, + 69126944884517, + 4697668176, + ], + [ + 82705914052438, + 75162694549094, + 13311535152, + 81427812211, + 1413862825910, + 60273972603, + 60273972603, + 0, + 0, + 10668333364687, + 3300575856848, + ], + [ + 236303039223047, + 202853013256559, + 35645164600, + 6771641359, + 1342465316601, + 495890410959, + 495890410959, + 0, + 0, + 67495866190051, + 32961884425988, + ], + [ + 365575060428990, + 340895878705702, + 102105623055, + 10621717111, + 1237706581885, + 569863013699, + 569863013699, + 0, + 0, + 92161982606740, + 59728850774291, + ], + [ + 257344719694972, + 229666411575042, + 80931427964, + 109163194883, + 800156367569, + 1397260273973, + 1397260273973, + 0, + 0, + 58566920815654, + 38322074234837, + ], + [ + 103419995574831, + 92967838230440, + 11560858739, + 16612232630, + 1166359847608, + 2501369863014, + 2501369863014, + 0, + 0, + 48397479051658, + 38875333994079, + ], + [ + 562720701100882, + 481488566864364, + 116651464120, + 59327031275, + 1211809168777, + 2739726027, + 2739726027, + 0, + 0, + 81250395890027, + 41983611, + ], + [ + 512540559562090, + 463370908894135, + 23975307191, + 62458043032, + 1381435252982, + 46575342466, + 46575342466, + 0, + 0, + 63300581685567, + 14644344612066, + ], + [ + 133491678730598, + 109347576257412, + 10509105736, + 65894314914, + 1164773497088, + 306849315068, + 306849315068, + 0, + 0, + 31762508568426, + 8820448018032, + ], + [ + 482702895290558, + 394108707450026, + 41877606476, + 72128595342, + 1171560526902, + 887671232877, + 887671232877, + 0, + 0, + 153517816742653, + 74345502113441, + ], + [ + 428638947151558, + 346534356899459, + 94694092284, + 35331170889, + 827613055222, + 1958904109589, + 1958904109589, + 0, + 0, + 144299708117383, + 54566905737505, + ], + [ + 158170289475608, + 144293813839117, + 56674203918, + 78607034772, + 1408556548718, + 2821917808219, + 2821917808219, + 0, + 0, + 79565622053793, + 71826653791298, + ], + [ + 200451489197906, + 187310868966629, + 27871466409, + 81639528826, + 1207748939980, + 16438356164, + 16438356164, + 0, + 0, + 15306549880957, + 2260467515608, + ], + [ + 508190738819872, + 445047250831291, + 33718179030, + 58796081642, + 1361861923105, + 73972602740, + 73972602740, + 0, + 0, + 79308484020057, + 16792195444892, + ], + [ + 267319421287696, + 236828921363025, + 64194144139, + 10537835035, + 1191740534930, + 306849315068, + 306849315068, + 0, + 0, + 56375214459764, + 24309843427676, + ], + [ + 468702079940345, + 437078137573746, + 95111185265, + 70271219282, + 1396129442237, + 843835616438, + 843835616438, + 0, + 0, + 147288826794225, + 113538586128250, + ], + [ + 460027776834762, + 422096926465184, + 32970596922, + 19755138867, + 1208083397045, + 1339726027397, + 1339726027397, + 0, + 0, + 164580829851878, + 124369434385901, + ], + [ + 374510743736165, + 339085010381382, + 26166085191, + 104223793854, + 1014446536579, + 2797260273973, + 2797260273973, + 0, + 0, + 127051164051093, + 129505583817683, + ], + [ + 507464750594877, + 413001692586937, + 29746602905, + 36119222175, + 1048558393281, + 2739726027, + 2739726027, + 0, + 0, + 94450930229163, + 86, + ], + [ + 183759827104722, + 173655826034556, + 74686927081, + 10792518044, + 1444930876303, + 41095890411, + 41095890411, + 0, + 0, + 17909568600217, + 7595803104736, + ], + [ + 319663137862052, + 286596049345816, + 104310284413, + 96822294847, + 1034833609125, + 117808219178, + 117808219178, + 0, + 0, + 44372370211529, + 11569829851654, + ], + [ + 511222304861942, + 445443665669906, + 94361861296, + 16853732029, + 1462479943299, + 972602739726, + 972602739726, + 0, + 0, + 198786317270644, + 120746268745704, + ], + [ + 127607803064405, + 119882130188137, + 50797104144, + 74981239833, + 1428834232006, + 1879452054795, + 1879452054795, + 0, + 0, + 56415122230548, + 51989429904782, + ], + [ + 130230480928235, + 111189299055260, + 85201760068, + 25423153576, + 933566928821, + 4841095890411, + 4841095890411, + 0, + 0, + 59839057834364, + 33462850364357, + ], + [ + 459413641461824, + 371609371659180, + 65719465995, + 81887567524, + 1376727876936, + 5479452055, + 5479452055, + 0, + 0, + 87753254408417, + 940152389, + ], + [ + 264102945049459, + 225674891663503, + 91021481570, + 75423530709, + 995282827686, + 27397260274, + 27397260274, + 0, + 0, + 38862841347113, + 474199622986, + ], + [ + 287345752003985, + 234810636910844, + 4755223266, + 200100593, + 1240790352464, + 249315068493, + 249315068493, + 0, + 0, + 69542886361236, + 16906985277693, + ], + [ + 503849020884489, + 421237382447025, + 26858727310, + 48129280000, + 1109143383750, + 871232876712, + 871232876712, + 0, + 0, + 152050731189440, + 75882326416549, + ], + [ + 488195737599292, + 403076610159137, + 16410289158, + 78932372219, + 1233179825390, + 1983561643836, + 1983561643836, + 0, + 0, + 204926018106045, + 150660909902836, + ], + [ + 599009006289007, + 490080617918370, + 8448669455, + 1175770898, + 1180258053494, + 4005479452055, + 4005479452055, + 0, + 0, + 376939970705150, + 263118967927086, + ], + [ + 442908486572739, + 380661440357522, + 97929288179, + 29625282349, + 1364453571001, + 2739726027, + 2739726027, + 0, + 0, + 62272268606427, + 487853720, + ], + [ + 367997546788338, + 320984287700014, + 117322909896, + 35256766614, + 1233748376000, + 41095890411, + 41095890411, + 0, + 0, + 51892239227072, + 4486847614141, + ], + [ + 598876187081232, + 483708431193369, + 76377407157, + 1382059421, + 1015523972546, + 273972602740, + 273972602740, + 0, + 0, + 141506240108104, + 22656837611248, + ], + [ + 269415473155727, + 245489155869362, + 78952064662, + 78542311281, + 1397892429727, + 767123287671, + 767123287671, + 0, + 0, + 82842780647067, + 60282714295498, + ], + [ + 46150109521298, + 38359535796323, + 37669496327, + 13722480665, + 1194023400895, + 1539726027397, + 1539726027397, + 0, + 0, + 18963362645598, + 10798958367838, + ], + [ + 59514397734721, + 54896033505472, + 118073557962, + 83144139588, + 1411830992322, + 3312328767123, + 3312328767123, + 0, + 0, + 28978193358630, + 23433834500351, + ], + [ + 62236000622660, + 57194286734308, + 51567507015, + 18155304370, + 898864405111, + 10958904110, + 10958904110, + 0, + 0, + 5133395229858, + 83140572172, + ], + [ + 76328776754947, + 66054430442757, + 68016396920, + 118548425449, + 1200411853432, + 71232876712, + 71232876712, + 0, + 0, + 11748005061019, + 1659864054430, + ], + [ + 313527704279367, + 289736764960662, + 34158477473, + 95862893913, + 1013128941683, + 454794520548, + 454794520548, + 0, + 0, + 57002985094802, + 37869972412624, + ], + [ + 507965317876963, + 406755271670848, + 38696363584, + 10619966665, + 1185500125603, + 835616438356, + 835616438356, + 0, + 0, + 173850412329296, + 70046179883780, + ], + [ + 263937336904879, + 236925074138161, + 83412857706, + 65771888990, + 1180988700228, + 1098630136986, + 1098630136986, + 0, + 0, + 82542583035133, + 55546829376608, + ], + [ + 429511042750645, + 348192889198026, + 7127848733, + 47202676208, + 857454116166, + 3835616438356, + 3835616438356, + 0, + 0, + 173025668682582, + 124436371522266, + ], + [ + 271422518066957, + 244482300988121, + 99108293875, + 100003560511, + 1375888528888, + 19178082192, + 19178082192, + 0, + 0, + 29483964908748, + 2597230312922, + ], + [ + 182943109910539, + 168027105820047, + 103588342783, + 62122451696, + 1000544740675, + 49315068493, + 49315068493, + 0, + 0, + 18397915197134, + 3371691333198, + ], + [ + 550189130921852, + 475729621176525, + 77257924988, + 102032138570, + 1188407298657, + 380821917808, + 380821917808, + 0, + 0, + 124340263795019, + 54551794261697, + ], + [ + 559457722805727, + 476094910035606, + 35575440965, + 48994222027, + 1221723383677, + 884931506849, + 884931506849, + 0, + 0, + 178854864525123, + 101281634897026, + ], + [ + 209043437176588, + 185173355704548, + 37522810662, + 34149848940, + 1427256323541, + 1542465753425, + 1542465753425, + 0, + 0, + 93656823554600, + 70614962276806, + ], + [ + 561402633497161, + 494275800063712, + 6133403198, + 49565984833, + 1035216886118, + 2852054794521, + 2852054794521, + 0, + 0, + 231178835699434, + 198015940923102, + ], + [ + 129015063259222, + 105674834992674, + 70326831153, + 4557157493, + 1083617081424, + 13698630137, + 13698630137, + 0, + 0, + 23384059359736, + 8225979266, + ], + [ + 79484088136167, + 74860150878097, + 63666783048, + 56691213812, + 1311164783078, + 46575342466, + 46575342466, + 0, + 0, + 7671368503864, + 3048247370596, + ], + [ + 360751954103508, + 326284570603084, + 78793762001, + 103506173345, + 1410397041312, + 104109589041, + 104109589041, + 0, + 0, + 55210612083224, + 21484699776332, + ], + [ + 165771861754931, + 141282485947208, + 9885056779, + 61341896340, + 1019157869188, + 583561643836, + 583561643836, + 0, + 0, + 39781930001990, + 17883433337695, + ], + [ + 430164697032301, + 366744199385907, + 10999314987, + 36479276525, + 830178730905, + 1084931506849, + 1084931506849, + 0, + 0, + 110674408148091, + 53827682747396, + ], + [ + 295502257633833, + 275744299076031, + 33539302595, + 49443985856, + 1163963938890, + 4032876712329, + 4032876712329, + 0, + 0, + 149125465112689, + 139970972935472, + ], + [ + 524346564123870, + 423778080893475, + 109323688875, + 72049020471, + 865254703880, + 13698630137, + 13698630137, + 0, + 0, + 100552574530575, + 897190085, + ], + [ + 110036847775124, + 96591507386352, + 88321750275, + 43043369294, + 1349239571388, + 60273972603, + 60273972603, + 0, + 0, + 16405086850492, + 2881642618719, + ], + [ + 38719560809665, + 33037983571380, + 11852041928, + 32306529799, + 896782176913, + 358904109589, + 358904109589, + 0, + 0, + 7727725709409, + 2211440194917, + ], + [ + 372236562612320, + 318750064124197, + 86602672566, + 66537841983, + 1086426731079, + 558904109589, + 558904109589, + 0, + 0, + 92824850648273, + 39869557342762, + ], + [ + 56392221235470, + 45759848047801, + 57055532016, + 18205327493, + 909356234962, + 1695890410959, + 1695890410959, + 0, + 0, + 19845365199465, + 8469529819098, + ], + [ + 108299958262333, + 88491124804651, + 103065634260, + 4875464327, + 1209864351134, + 4200000000000, + 4200000000000, + 0, + 0, + 62109876570413, + 32561332533948, + ], + [ + 34139074860102, + 28806387997188, + 24579217006, + 23253309379, + 1422544405696, + 10958904110, + 10958904110, + 0, + 0, + 5356054335555, + 24555735354, + ], + [ + 438541551531193, + 402874318927526, + 47908371482, + 104276671199, + 1181375348577, + 49315068493, + 49315068493, + 0, + 0, + 46599591130683, + 11624055522296, + ], + [ + 215942037003729, + 186318916922258, + 67529090628, + 58659489404, + 854389474772, + 268493150685, + 268493150685, + 0, + 0, + 38198708759864, + 8855142845107, + ], + [ + 292951841593411, + 253699459736302, + 45244019259, + 28317112198, + 1416379614816, + 515068493151, + 515068493151, + 0, + 0, + 86087570410331, + 46488070548516, + ], + [ + 525568560050030, + 482069606230058, + 24543790286, + 50667446230, + 891421592987, + 1890410958904, + 1890410958904, + 0, + 0, + 153700105890962, + 124360939962996, + ], + [ + 89594677070934, + 73600796352342, + 61418515217, + 112738550576, + 1428320779177, + 2205479452055, + 2205479452055, + 0, + 0, + 41536762917499, + 31834665156161, + ], + [ + 293796596909364, + 277545549131701, + 36148108538, + 6760587713, + 1200680550902, + 2739726027, + 2739726027, + 0, + 0, + 16520192342927, + 258927442998, + ], + [ + 218028913877736, + 178032769184794, + 85135888369, + 25890942187, + 1461935998748, + 79452054795, + 79452054795, + 0, + 0, + 45332840125519, + 5095838397661, + ], + [ + 396646138946524, + 327136268324332, + 63757793348, + 52424771006, + 1128630832834, + 317808219178, + 317808219178, + 0, + 0, + 93892108386389, + 25075706521632, + ], + [ + 340794035278819, + 281219197239705, + 62262350658, + 69509466454, + 1190029244161, + 638356164384, + 638356164384, + 0, + 0, + 98753765173260, + 42256737887334, + ], + [ + 429909813933997, + 375043599301237, + 48296583078, + 110675264901, + 877451550512, + 2000000000000, + 2000000000000, + 0, + 0, + 117412881516665, + 90964754805521, + ], + [ + 110969888939413, + 105525757947190, + 15906045465, + 89575340299, + 1362879314811, + 3369863013699, + 3369863013699, + 0, + 0, + 56861012931059, + 63740047486463, + ], + [ + 540381173250120, + 457152884067596, + 67209246100, + 106855448257, + 1024172356797, + 5479452055, + 5479452055, + 0, + 0, + 83139317147595, + 342988636, + ], + [ + 517813171633512, + 452760777709013, + 73633599645, + 53711199518, + 1187950138714, + 82191780822, + 82191780822, + 0, + 0, + 79023036513864, + 13941535089110, + ], + [ + 316436584022813, + 282364469725181, + 24210936163, + 66924453277, + 1272678104549, + 298630136986, + 298630136986, + 0, + 0, + 65551519398172, + 33719991397293, + ], + [ + 123535784080937, + 102809398078954, + 103559914844, + 62327267012, + 1023497021859, + 556164383562, + 556164383562, + 0, + 0, + 31437496796996, + 10523675802044, + ], + [ + 179912569232937, + 146461844498703, + 35301918279, + 43905315411, + 928757710488, + 1972602739726, + 1972602739726, + 0, + 0, + 63672321981640, + 33887678955042, + ], + [ + 134944499745659, + 110514962710987, + 35863590022, + 79143962540, + 1083714308191, + 4989041095890, + 4989041095890, + 0, + 0, + 63185915047291, + 54109474553643, + ], + [ + 258854791396955, + 218970330559074, + 89791821485, + 51019620610, + 1269808044732, + 8219178082, + 8219178082, + 0, + 0, + 39926182490694, + 29926816840, + ], + [ + 499877222687559, + 471779807727048, + 4254358961, + 39281151353, + 1370587398408, + 35616438356, + 35616438356, + 0, + 0, + 44957227779242, + 17175698928950, + ], + [ + 198029705663761, + 169076724568923, + 103484715182, + 103535319074, + 1130706094686, + 200000000000, + 200000000000, + 0, + 0, + 38193901529796, + 9834981574018, + ], + [ + 398964674327455, + 325094514981512, + 89958517457, + 14280899266, + 1029863492330, + 769863013699, + 769863013699, + 0, + 0, + 120055678687252, + 40070148327765, + ], + [ + 511573832819868, + 423056077976358, + 13198886198, + 9310869465, + 819909163450, + 1326027397260, + 1326027397260, + 0, + 0, + 152314960897799, + 64034883259160, + ], + [ + 355460483359387, + 289762909853648, + 62100687945, + 57495105126, + 1005715522832, + 4830136986301, + 4830136986301, + 0, + 0, + 161212519652301, + 109589403824928, + ], + [ + 188397995979932, + 169800225308318, + 57387262445, + 76330321807, + 1352588754463, + 2739726027, + 2739726027, + 0, + 0, + 18602828474883, + 12869576203, + ], + [ + 311680002839070, + 250953987018623, + 55790639961, + 35218361887, + 1249802141466, + 76712328767, + 76712328767, + 0, + 0, + 64672723471002, + 3961032269691, + ], + [ + 566010078567774, + 462038349497276, + 88386627063, + 23920567463, + 1178203407674, + 389041095890, + 389041095890, + 0, + 0, + 149992013270315, + 42619160799034, + ], + [ + 524167493346731, + 466621038652624, + 48664447656, + 87931957999, + 1497964741272, + 602739726027, + 602739726027, + 0, + 0, + 158627843385904, + 108721299248027, + ], + [ + 260644125475181, + 235303531120989, + 48429070945, + 107478439375, + 1405219972006, + 1545205479452, + 1545205479452, + 0, + 0, + 102608645420952, + 89800747391479, + ], + [ + 53742200070174, + 47938911017599, + 93887318726, + 10193316051, + 1408335967760, + 2454794520548, + 2454794520548, + 0, + 0, + 28996145470944, + 19686779652178, + ], + [ + 93082578760981, + 79599356133454, + 1560543062, + 5253837048, + 860883443882, + 13698630137, + 13698630137, + 0, + 0, + 13486093871390, + 5514052313, + ], + [ + 259049364869814, + 237957080240332, + 29621881120, + 48430587703, + 1196867373593, + 82191780822, + 82191780822, + 0, + 0, + 31788300433323, + 10946951334525, + ], + [ + 104888965025569, + 98711000858407, + 94818702155, + 66621540284, + 1189018713573, + 460273972603, + 460273972603, + 0, + 0, + 21981741223997, + 15413192305132, + ], + [ + 391800585951832, + 323388302410407, + 109211292821, + 83943549964, + 1025310373474, + 947945205479, + 947945205479, + 0, + 0, + 114032766452148, + 48083878406310, + ], + [ + 136437869856537, + 121790150773276, + 5490618008, + 96788720026, + 1029353991917, + 1268493150685, + 1268493150685, + 0, + 0, + 37557329392615, + 30562642403095, + ], + [ + 413122246447590, + 353032023422132, + 7193368689, + 64055135225, + 973939917641, + 4189041095890, + 4189041095890, + 0, + 0, + 177802861522601, + 163662695336902, + ], + [ + 485673434336089, + 460213241270639, + 76286889159, + 2531209492, + 1325701231268, + 19178082192, + 19178082192, + 0, + 0, + 35462887218765, + 9696765787564, + ], + [ + 257751447300750, + 222072379688999, + 70165745113, + 94846417109, + 879733578822, + 30136986301, + 30136986301, + 0, + 0, + 35908240262242, + 400171031312, + ], + [ + 508277694200202, + 435220853401199, + 14170261836, + 21436081855, + 992486820653, + 317808219178, + 317808219178, + 0, + 0, + 103621805145146, + 31476985914024, + ], + [ + 497223564028560, + 452898929113166, + 41090445669, + 44299470430, + 1483885889456, + 613698630137, + 613698630137, + 0, + 0, + 150796056493430, + 108052291321182, + ], + [ + 551566353474236, + 482991409987266, + 96057347352, + 83307308333, + 1283743819938, + 1452054794521, + 1452054794521, + 0, + 0, + 204754416901533, + 140638653818337, + ], + [ + 26631405528460, + 21726795413035, + 65915147956, + 31156367568, + 1361966999353, + 3019178082192, + 3019178082192, + 0, + 0, + 15027910097341, + 9822022369415, + ], + [ + 148440863453467, + 121071421210583, + 97496475643, + 64598720941, + 1367417260093, + 10958904110, + 10958904110, + 0, + 0, + 27391590631758, + 24642940770, + ], + [ + 490857764490405, + 401958006790363, + 65478905816, + 97893736099, + 1284143321577, + 27397260274, + 27397260274, + 0, + 0, + 89724466285822, + 1201551515680, + ], + [ + 265717239617228, + 237045930658637, + 8234823421, + 77106640753, + 1230997397388, + 241095890411, + 241095890411, + 0, + 0, + 49808232598442, + 23383358444705, + ], + [ + 418304276529473, + 398104247054335, + 109972365571, + 38965172899, + 1325419895474, + 638356164384, + 638356164384, + 0, + 0, + 111740454358208, + 83937102306882, + ], + [ + 90738813953235, + 73356956858017, + 26275521793, + 68933754628, + 980683303459, + 1076712328767, + 1076712328767, + 0, + 0, + 27198175099041, + 12296236733127, + ], + [ + 189667831393350, + 178476572967478, + 61190330565, + 53333793186, + 1137510705980, + 3030136986301, + 3030136986301, + 0, + 0, + 82032742452867, + 70844960964615, + ], + [ + 118583165120994, + 96089577912297, + 13446143852, + 54345569632, + 818390022626, + 5479452055, + 5479452055, + 0, + 0, + 22478644344823, + 549, + ], + [ + 204377639301830, + 175861890201006, + 111990908066, + 100798736798, + 1001464039191, + 27397260274, + 27397260274, + 0, + 0, + 28910979321697, + 451347560951, + ], + [ + 333063069079644, + 274694981671108, + 7837161346, + 5431844388, + 955698383869, + 383561643836, + 383561643836, + 0, + 0, + 77110058789799, + 18763938567295, + ], + [ + 27172125008799, + 22485055664602, + 46835735742, + 48488780203, + 1228341522410, + 616438356164, + 616438356164, + 0, + 0, + 8041623152946, + 3501386645455, + ], + [ + 450663786178535, + 372271807775546, + 105226258069, + 82643430320, + 1001739898575, + 1378082191781, + 1378082191781, + 0, + 0, + 142057473265344, + 68118002274240, + ], + [ + 248000616523744, + 204801617543358, + 78621689317, + 83996709861, + 1065184603020, + 3849315068493, + 3849315068493, + 0, + 0, + 102792976163526, + 72757350258564, + ], + [ + 437947250741024, + 414603729222863, + 17188362772, + 36000660419, + 1355365802819, + 19178082192, + 19178082192, + 0, + 0, + 32349022502613, + 9092161270568, + ], + [ + 171494009473933, + 140079863013967, + 68918803564, + 57529025619, + 826718791187, + 82191780822, + 82191780822, + 0, + 0, + 31966306770691, + 649761335425, + ], + [ + 47382365120460, + 38855057444044, + 68488749585, + 114966230945, + 869767091226, + 101369863014, + 101369863014, + 0, + 0, + 8721828817363, + 364191485871, + ], + [ + 243322989661001, + 219865978737570, + 9153275547, + 38699387308, + 1224894093689, + 958904109589, + 958904109589, + 0, + 0, + 75512641730066, + 55645352487409, + ], + [ + 105377387466106, + 84836656241961, + 18221989304, + 112597059795, + 1377350366172, + 1904109589041, + 1904109589041, + 0, + 0, + 46554610170161, + 35335600781770, + ], + [ + 303443194706788, + 283193505676036, + 4372275564, + 10516239776, + 1234995371059, + 4939726027397, + 4939726027397, + 0, + 0, + 204961137132889, + 189605154392066, + ], + [ + 289844811016615, + 249415751120540, + 81975404678, + 80564801861, + 999673660363, + 13698630137, + 13698630137, + 0, + 0, + 40469588064038, + 83105271131, + ], + [ + 394633339439157, + 326270977715221, + 84399691955, + 51867528525, + 1418528767885, + 79452054795, + 79452054795, + 0, + 0, + 77928492542966, + 9515974791071, + ], + [ + 25515083627977, + 23615038191674, + 6619149315, + 1079084821, + 1322493218788, + 454794520548, + 454794520548, + 0, + 0, + 6162583890147, + 4236175559535, + ], + [ + 185288821006405, + 167487952968999, + 92152052926, + 12142012162, + 1498493575170, + 871232876712, + 871232876712, + 0, + 0, + 68429008206646, + 45900725442206, + ], + [ + 335950138037070, + 312636013530736, + 31236973137, + 14358177548, + 1232888590421, + 1358904109589, + 1358904109589, + 0, + 0, + 122698797357668, + 96632539867927, + ], + [ + 444967656058402, + 415747401617654, + 31810929141, + 33454969483, + 1057167876215, + 3232876712329, + 3232876712329, + 0, + 0, + 196364595111804, + 171065063389452, + ], + [ + 182692152563806, + 147337386473838, + 113406136237, + 66359870352, + 885618220221, + 13698630137, + 13698630137, + 0, + 0, + 35359063356787, + 387567847, + ], + [ + 294007643783886, + 241211738722728, + 5496030461, + 73755482940, + 1330865760368, + 65753424658, + 65753424658, + 0, + 0, + 56581498294865, + 4463241457726, + ], + [ + 207413919147219, + 171788341718991, + 112622211435, + 92386694635, + 1490498288585, + 391780821918, + 391780821918, + 0, + 0, + 59744072960764, + 24867464565194, + ], + [ + 243108734476230, + 207031705668025, + 85625000495, + 22834347434, + 1306357984559, + 550684931507, + 550684931507, + 0, + 0, + 71282025436544, + 32810511722808, + ], + [ + 105091272061973, + 87152127487821, + 80459440961, + 74180420958, + 1025879007220, + 1482191780822, + 1482191780822, + 0, + 0, + 34570356744396, + 18212525581944, + ], + [ + 440897062653489, + 412630554316207, + 18960502671, + 95259627726, + 1209916346896, + 2271232876712, + 2271232876712, + 0, + 0, + 170404405403875, + 177896091625559, + ], + [ + 62921971781390, + 55915704390800, + 100927727332, + 31994275595, + 1340882830916, + 8219178082, + 8219178082, + 0, + 0, + 7097221344594, + 78949910770, + ], + [ + 67352763488856, + 62994312581404, + 23519885015, + 29484672319, + 1007432527197, + 24657534247, + 24657534247, + 0, + 0, + 5157210021887, + 806235937076, + ], + [ + 138325245092425, + 115809488014708, + 76167207592, + 44804288442, + 1255263132566, + 276712328767, + 276712328767, + 0, + 0, + 32569217500450, + 9933631844233, + ], + [ + 108760310343343, + 103098343137164, + 50342208999, + 33008115899, + 1268355104667, + 794520547945, + 794520547945, + 0, + 0, + 30728907155819, + 24566048138537, + ], + [ + 379450485795302, + 326088996734713, + 90808437542, + 49875002503, + 1241206043671, + 1046575342466, + 1046575342466, + 0, + 0, + 129676660802352, + 73655387118803, + ], + [ + 101232525252991, + 87958439779894, + 33491927918, + 36577440645, + 1339963968005, + 3178082191781, + 3178082191781, + 0, + 0, + 57153004251241, + 45663990164486, + ], + [ + 355269362790402, + 328512037178140, + 73821631878, + 115277426528, + 1378502067240, + 13698630137, + 13698630137, + 0, + 0, + 30210708096203, + 3581182791225, + ], + [ + 347798849158508, + 278461597132948, + 60974206394, + 14325239415, + 1245546337185, + 65753424658, + 65753424658, + 0, + 0, + 72750744179206, + 3159110766393, + ], + [ + 442080419344642, + 372351284097073, + 63083212273, + 79622129872, + 886582335076, + 112328767123, + 112328767123, + 0, + 0, + 74682291407351, + 5853010149413, + ], + [ + 245601076064858, + 221676446032776, + 76839000252, + 34235440731, + 1153522102535, + 884931506849, + 884931506849, + 0, + 0, + 71380419616425, + 44648261205632, + ], + [ + 237023565543719, + 220543970782946, + 27155150260, + 43575080119, + 1465397636880, + 1863013698630, + 1863013698630, + 0, + 0, + 113946868363808, + 101691864895732, + ], + [ + 507614769313132, + 437568636312449, + 78057623807, + 24667680270, + 843991994498, + 3498630136986, + 3498630136986, + 0, + 0, + 198723766915065, + 106982516878156, + ], + [ + 28415200177043, + 24031143454046, + 17698778962, + 118998296531, + 1377597510016, + 16438356164, + 16438356164, + 0, + 0, + 4416335677221, + 57192717002, + ], + ]; + // Covers the full-corpus max (<595,000,000 raw), which occurs + // in QuantLib's cancellation-sensitive two-day Levy tail. + const TOLERANCE: u128 = 1_000_000_000; // $0.001 + let mut max_call = (0u128, 0usize); + let mut max_put = (0u128, 0usize); + for (index, vector) in VECTORS.iter().enumerate() { + let [s, k, r, q, sigma, t, averaging_time, fixed_average, fixed_weight, expected_call, expected_put] = + *vector; + let actual = arithmetic_asian_price( + s, + k, + r, + q, + sigma, + t, + averaging_time, + fixed_average, + fixed_weight, + ) + .unwrap(); + let call_diff = actual.call.abs_diff(expected_call); + let put_diff = actual.put.abs_diff(expected_put); + if call_diff > max_call.0 { + max_call = (call_diff, index); + } + if put_diff > max_put.0 { + max_put = (put_diff, index); + } + } + eprintln!( + "max QuantLib diffs: call={} at {}, put={} at {}", + max_call.0, max_call.1, max_put.0, max_put.1 + ); + assert!( + max_call.0 <= TOLERANCE, + "QuantLib call max diff={} at {}", + max_call.0, + max_call.1 + ); + assert!( + max_put.0 <= TOLERANCE, + "QuantLib put max diff={} at {}", + max_put.0, + max_put.1 + ); +} diff --git a/tests/asian_reference.rs b/tests/asian_reference.rs new file mode 100644 index 0000000..bcc3d88 --- /dev/null +++ b/tests/asian_reference.rs @@ -0,0 +1,193 @@ +//! Independent high-precision references for continuous arithmetic-Asian marks. +//! +//! Expected values were generated with mpmath at 80 decimal digits from the +//! closed-form first two GBM average moments and an independently implemented +//! lognormal moment match. The separately generated QuantLib corpus lives in +//! `benchmark/asian_quantlib_vectors.json` and is exercised by +//! `tests/asian_quantlib_reference.rs`. + +#![cfg(feature = "asian")] + +use solmath::arithmetic_asian_price; + +#[derive(Clone, Copy)] +struct Vector { + inputs: [u128; 9], + call: u128, + put: u128, + mean: u128, + log_variance: u128, +} + +const VECTORS: [Vector; 6] = [ + Vector { + inputs: [ + 100_000_000_000_000, + 100_000_000_000_000, + 50_000_000_000, + 20_000_000_000, + 400_000_000_000, + 1_000_000_000_000, + 1_000_000_000_000, + 0, + 0, + ], + call: 9_641_361_495_792, + put: 8_200_141_259_059, + mean: 101_515_113_178_390, + log_variance: 54_454_422_633, + }, + Vector { + // A 30-day averaging window beginning 335 days from now. + inputs: [ + 100_000_000_000_000, + 105_000_000_000_000, + 30_000_000_000, + 10_000_000_000, + 600_000_000_000, + 1_000_000_000_000, + 82_191_780_821, + 0, + 0, + ], + call: 21_583_068_184_745, + put: 24_556_195_221_206, + mean: 101_936_327_765_259, + log_variance: 340_302_385_996, + }, + Vector { + // 180/365 of the final average is already fixed at 98. + inputs: [ + 100_000_000_000_000, + 100_000_000_000_000, + 50_000_000_000, + 20_000_000_000, + 400_000_000_000, + 506_849_315_068, + 506_849_315_068, + 98_000_000_000_000, + 493_150_684_931, + ], + call: 3_025_021_155_572, + put: 3_609_028_308_725, + mean: 99_401_003_534_725, + log_variance: 7_284_694_054, + }, + Vector { + inputs: [ + 80_000_000_000_000, + 100_000_000_000_000, + 0, + 0, + 200_000_000_000, + 82_191_780_821, + 82_191_780_821, + 0, + 0, + ], + call: 3, + put: 20_000_000_000_003, + mean: 80_000_000_000_000, + log_variance: 1_096_190_699, + }, + Vector { + inputs: [ + 120_000_000_000_000, + 100_000_000_000_000, + 100_000_000_000, + 40_000_000_000, + 800_000_000_000, + 2_000_000_000_000, + 2_000_000_000_000, + 0, + 0, + ], + call: 38_152_241_931_369, + put: 15_639_723_930_513, + mean: 127_496_851_579_376, + log_variance: 488_312_882_027, + }, + Vector { + // Twelve of thirty TWAP minutes fixed at 99.50; eighteen remain. + inputs: [ + 100_000_000_000_000, + 100_000_000_000_000, + 50_000_000_000, + 20_000_000_000, + 600_000_000_000, + 34_246_575, + 34_246_575, + 99_500_000_000_000, + 400_000_000_000, + ], + call: 2_558_829_669, + put: 202_527_665_328, + mean: 99_800_030_821_928, + log_variance: 1_485_392, + }, +]; + +fn assert_close(actual: u128, expected: u128, tolerance: u128, label: &str, index: usize) { + assert!( + actual.abs_diff(expected) <= tolerance, + "vector {index} {label}: actual={actual} expected={expected} diff={}", + actual.abs_diff(expected) + ); +} + +#[test] +fn matches_high_precision_moment_match() { + for (index, vector) in VECTORS.iter().enumerate() { + let [s, k, r, q, sigma, t, averaging_time, fixed_average, fixed_weight] = vector.inputs; + let actual = arithmetic_asian_price( + s, + k, + r, + q, + sigma, + t, + averaging_time, + fixed_average, + fixed_weight, + ) + .unwrap(); + + // The final option transform uses the certified SCALE CDF for SBF + // efficiency while the cancellation-sensitive moments stay at HP. + // Its absolute price budget remains below 5e-9 real units. + assert_close(actual.call, vector.call, 5_000, "call", index); + assert_close(actual.put, vector.put, 5_000, "put", index); + assert_close(actual.expected_average, vector.mean, 10, "mean", index); + assert_close( + actual.log_variance, + vector.log_variance, + 1_000, + "log_variance", + index, + ); + } +} + +#[test] +fn put_call_parity_is_exact_for_every_reference_vector() { + for vector in VECTORS { + let [s, k, r, q, sigma, t, averaging_time, fixed_average, fixed_weight] = vector.inputs; + let actual = arithmetic_asian_price( + s, + k, + r, + q, + sigma, + t, + averaging_time, + fixed_average, + fixed_weight, + ) + .unwrap(); + // The reference values themselves satisfy parity to their independent + // rounding tolerance; exact public parity is covered internally using + // the returned mean and the crate's discount path. + assert!(actual.call <= actual.expected_average); + assert!(actual.put <= k.max(actual.expected_average)); + } +} diff --git a/tests/checked_layer.rs b/tests/checked_layer.rs new file mode 100644 index 0000000..0388d27 --- /dev/null +++ b/tests/checked_layer.rs @@ -0,0 +1,380 @@ +//! Contract tests for the safe-by-construction [`solmath::checked`] layer. +//! +//! These establish, reproducibly and in CI, the property the layer exists to +//! provide: **once inputs are validated into the typed bundles, no pricing +//! method panics or silently wraps for any in-domain input.** A returned `Err` +//! is a valid, handled outcome (errors-as-values); an unwinding panic is not, +//! because on-chain it aborts the instruction. +//! +//! The domain sweep is deterministic (a fixed xorshift seed, no external +//! crates). Its iteration count defaults low enough for CI but can be scaled up +//! for soak testing with `SOLMATH_FUZZ_ITERS`, e.g. +//! +//! ```text +//! SOLMATH_FUZZ_ITERS=20000000 cargo test --features full --test checked_layer -- --nocapture +//! ``` + +#![cfg(feature = "bs")] + +use solmath::{EuropeanInputs, Price, Rate, SolMathError, Time, Vol, SCALE}; + +fn fuzz_iters(default: u64) -> u64 { + std::env::var("SOLMATH_FUZZ_ITERS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(default) +} + +// xorshift64 — deterministic, dependency-free. +struct Rng(u64); +impl Rng { + fn next(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + /// A value in `[0, max]`, biased to include 0, max, and their neighbours. + fn in_domain(&mut self, max: u128) -> u128 { + match self.next() % 8 { + 0 => 0, + 1 => max, + 2 => max.saturating_sub(1), + 3 => 1, + 4 => max / 2, + _ => { + let raw = (self.next() as u128) << 64 | self.next() as u128; + match max.checked_add(1) { + Some(m) => raw % m, + None => raw, // max == u128::MAX: full range, no modulo + } + } + } + } +} + +#[test] +fn constructors_enforce_domain_boundaries() { + // Accept exactly at the maximum. + assert!(Price::new(Price::MAX).is_ok()); + assert!(Rate::new(Rate::MAX).is_ok()); + assert!(Vol::new(Vol::MAX).is_ok()); + assert!(Time::new(Time::MAX).is_ok()); + + // Reject one past the maximum. + assert!(matches!( + Price::new(Price::MAX + 1), + Err(SolMathError::DomainError) + )); + assert!(matches!( + Rate::new(Rate::MAX + 1), + Err(SolMathError::DomainError) + )); + assert!(matches!( + Vol::new(Vol::MAX + 1), + Err(SolMathError::DomainError) + )); + assert!(matches!( + Time::new(Time::MAX + 1), + Err(SolMathError::DomainError) + )); + + // Zero: allowed for Price/Rate, rejected for Vol/Time (BS needs sigma,t > 0). + assert!(Price::new(0).is_ok()); + assert!(Rate::new(0).is_ok()); + assert!(matches!(Vol::new(0), Err(SolMathError::DomainError))); + assert!(matches!(Time::new(0), Err(SolMathError::DomainError))); + + // Bundle validation rejects if any single field is out of range. + assert!(EuropeanInputs::from_raw(Price::MAX + 1, 1, 0, 1, 1).is_err()); + assert!(EuropeanInputs::from_raw(1, 1, 0, 0, 1).is_err()); // sigma == 0 + assert!(EuropeanInputs::from_raw(1, 1, 0, 1, 0).is_err()); // t == 0 + // Every field at its ceiling still constructs. + assert!( + EuropeanInputs::from_raw(Price::MAX, Price::MAX, Rate::MAX, Vol::MAX, Time::MAX).is_ok() + ); +} + +#[test] +fn checked_bundle_matches_the_raw_functions_exactly() { + // The wrapper must be a pure pass-through: identical results to the raw + // functions for validated inputs, adding validation but no behaviour change. + use solmath::{black_scholes_price, bs_delta, bs_full, bs_gamma, bs_rho, bs_theta, bs_vega}; + + let cases = [ + (100u128, 105u128, 5u128, 20u128, 1u128), + (1, 1, 0, 1, 1), + (50_000, 40_000, 3, 80, 2), + (100, 100, 0, 200, 1), + ]; + let scale = 1_000_000_000_000u128; + for (s, k, r_pct, sig_pct, t_y) in cases { + let s = s * scale; + let k = k * scale; + let r = r_pct * scale / 100; + let sigma = sig_pct * scale / 100; + let t = t_y * scale; + + let inp = EuropeanInputs::from_raw(s, k, r, sigma, t).unwrap(); + assert_eq!( + inp.price().ok(), + black_scholes_price(s, k, r, sigma, t).ok() + ); + assert_eq!( + inp.full().map(|f| (f.call, f.put)).ok(), + bs_full(s, k, r, sigma, t).map(|f| (f.call, f.put)).ok() + ); + assert_eq!(inp.delta().ok(), bs_delta(s, k, r, sigma, t).ok()); + assert_eq!(inp.gamma().ok(), bs_gamma(s, k, r, sigma, t).ok()); + assert_eq!(inp.vega().ok(), bs_vega(s, k, r, sigma, t).ok()); + assert_eq!(inp.theta().ok(), bs_theta(s, k, r, sigma, t).ok()); + assert_eq!(inp.rho().ok(), bs_rho(s, k, r, sigma, t).ok()); + } +} + +#[test] +fn checked_inputs_never_panic_over_domain() { + let iters = fuzz_iters(200_000); + let mut rng = Rng(0x0913_2f7c_a5e1_bb42); + let mut bs_ok = 0u64; + let mut bs_err = 0u64; + + for _ in 0..iters { + let s = rng.in_domain(Price::MAX); + let k = rng.in_domain(Price::MAX); + let r = rng.in_domain(Rate::MAX); + let sigma = 1 + rng.in_domain(Vol::MAX - 1); + let t = 1 + rng.in_domain(Time::MAX - 1); + + // Every draw is in-domain, so construction must succeed. + let inp = EuropeanInputs::from_raw(s, k, r, sigma, t) + .expect("in-domain construction must succeed"); + + // No method may panic. Under debug assertions this also exercises the + // `mul_fast` / `fp_mul_i_fast` overflow preconditions inside the kernels. + let outcome = std::panic::catch_unwind(|| { + let full = inp.full(); + let _ = inp.price(); + let _ = inp.delta(); + let _ = inp.gamma(); + let _ = inp.vega(); + let _ = inp.theta(); + let _ = inp.rho(); + full.is_ok() + }); + match outcome { + Ok(true) => bs_ok += 1, + Ok(false) => bs_err += 1, + Err(_) => { + panic!("checked EuropeanInputs panicked for s={s} k={k} r={r} sigma={sigma} t={t}") + } + } + } + + // Sanity: the sweep must actually reach the successful pricing path, not + // just the degenerate Err corners, or it would prove nothing. + assert!( + bs_ok > iters / 2, + "expected a majority of in-domain inputs to price successfully (ok={bs_ok}, err={bs_err})" + ); +} + +#[cfg(feature = "iv")] +#[test] +fn checked_implied_vol_never_panics_over_domain() { + use solmath::ImpliedVolInputs; + + let iters = fuzz_iters(200_000); + let mut rng = Rng(0x51ab_c0de_1234_9e37); + for _ in 0..iters { + let mp = rng.in_domain(Price::MAX); + let s = rng.in_domain(Price::MAX); + let k = rng.in_domain(Price::MAX); + let r = rng.in_domain(Rate::MAX); + let t = 1 + rng.in_domain(Time::MAX - 1); + + let inp = ImpliedVolInputs::from_raw(mp, s, k, r, t) + .expect("in-domain construction must succeed"); + let outcome = std::panic::catch_unwind(|| inp.solve()); + assert!( + outcome.is_ok(), + "checked ImpliedVolInputs panicked for mp={mp} s={s} k={k} r={r} t={t}" + ); + } +} + +#[cfg(feature = "barrier")] +#[test] +fn checked_barrier_never_panics_over_domain() { + use solmath::barrier::BarrierType; + use solmath::BarrierInputs; + + const TYPES: [BarrierType; 4] = [ + BarrierType::DownAndOut, + BarrierType::DownAndIn, + BarrierType::UpAndOut, + BarrierType::UpAndIn, + ]; + + let iters = fuzz_iters(100_000); + let mut rng = Rng(0x7a1e_9d3c_44b0_1122); + for _ in 0..iters { + let s = rng.in_domain(Price::MAX); + let k = rng.in_domain(Price::MAX); + let h = rng.in_domain(Price::MAX); + let r = rng.in_domain(Rate::MAX); + let sigma = 1 + rng.in_domain(Vol::MAX - 1); + let t = 1 + rng.in_domain(Time::MAX - 1); + + let inp = BarrierInputs::from_raw(s, k, h, r, sigma, t) + .expect("in-domain construction must succeed"); + let ty = TYPES[(rng.next() % 4) as usize]; + let is_call = rng.next() & 1 == 0; + let breached = rng.next() & 1 == 0; + let outcome = std::panic::catch_unwind(|| { + let _ = inp.price(is_call, ty); + let _ = inp.price_with_state(is_call, ty, breached); + }); + assert!( + outcome.is_ok(), + "checked BarrierInputs panicked for s={s} k={k} h={h} r={r} sigma={sigma} t={t}" + ); + } +} + +#[cfg(feature = "asian")] +#[test] +fn checked_twap_validates_state_and_never_panics() { + use solmath::{twap_option_price, TwapInputs}; + + let raw = [ + 100 * SCALE, + 100 * SCALE, + 50_000_000_000, + 20_000_000_000, + 600_000_000_000, + 18 * SCALE / (365 * 24 * 60), + 18 * SCALE / (365 * 24 * 60), + 99_500_000_000_000, + 400_000_000_000, + ]; + let checked = TwapInputs::from_raw( + raw[0], raw[1], raw[2], raw[3], raw[4], raw[5], raw[6], raw[7], raw[8], + ) + .unwrap(); + assert_eq!( + checked.price(), + twap_option_price(raw[0], raw[1], raw[2], raw[3], raw[4], raw[5], raw[6], raw[7], raw[8]) + ); + + assert!(TwapInputs::from_raw(100, 100, 0, 0, 1, SCALE, SCALE + 1, 0, 0).is_err()); + assert!(TwapInputs::from_raw(100, 100, 0, 0, 1, SCALE, SCALE, 100, 0).is_err()); + assert!(TwapInputs::from_raw(100, 100, 0, 0, 1, SCALE, 0, 100, SCALE / 2).is_err()); + + let iters = fuzz_iters(50_000); + let mut rng = Rng(0xa51a_7a2a_5eed_2026); + for _ in 0..iters { + let s = (1 + rng.in_domain(1_000)) * SCALE; + let k = (1 + rng.in_domain(1_000)) * SCALE; + let r = rng.in_domain(SCALE / 2); + let q = rng.in_domain(SCALE / 2); + let sigma = 1 + rng.in_domain(3 * SCALE - 1); + let t = 1 + rng.in_domain(2 * SCALE - 1); + let averaging_time = if t == 1 { + 1 + } else { + 1 + rng.in_domain(t - 1).min(t - 1) + }; + let fixed_weight = rng.in_domain(SCALE - 1); + let fixed_average = if fixed_weight == 0 { + 0 + } else { + (1 + rng.in_domain(1_000)) * SCALE + }; + + let inputs = TwapInputs::from_raw( + s, + k, + r, + q, + sigma, + t, + averaging_time, + fixed_average, + fixed_weight, + ) + .unwrap_or_else(|error| { + panic!( + "generated TWAP state rejected ({error:?}) for s={s} k={k} r={r} q={q} sigma={sigma} t={t} averaging_time={averaging_time} fixed_average={fixed_average} fixed_weight={fixed_weight}" + ) + }); + let outcome = std::panic::catch_unwind(|| inputs.price()); + assert!( + outcome.is_ok(), + "checked TwapInputs panicked for s={s} k={k} r={r} q={q} sigma={sigma} t={t} averaging_time={averaging_time} fixed_average={fixed_average} fixed_weight={fixed_weight}" + ); + } +} + +#[cfg(feature = "pool")] +#[test] +fn checked_pool_swap_validates_domain_and_never_panics() { + use solmath::{weighted_pool_swap, PoolSwapInputs}; + + // A validated bundle must quote identically to the raw kernel. + let inp = PoolSwapInputs::from_raw( + 1_000_000 * SCALE, // balance_in + 2_000_000 * SCALE, // balance_out + 2, // weight_in + 1, // weight_out (ratio 2 <= 20) + 10_000 * SCALE, // amount_in + 3_000_000_000, // 0.3% fee + ) + .expect("balanced swap is in the certified domain"); + assert_eq!( + inp.quote().ok(), + weighted_pool_swap( + 1_000_000 * SCALE, + 2_000_000 * SCALE, + 2, + 1, + 10_000 * SCALE, + 3_000_000_000 + ) + .ok() + ); + + // Domain rejections mirror the kernel's guards. + assert!(PoolSwapInputs::from_raw(0, 1, 1, 1, 1, 0).is_err()); // zero balance + assert!(PoolSwapInputs::from_raw(1, 1, 0, 1, 1, 0).is_err()); // zero weight + assert!(PoolSwapInputs::from_raw(1, 1, 21, 1, 1, 0).is_err()); // weight ratio > 20 + assert!(PoolSwapInputs::from_raw(1, 1, 1, 1, 1, SCALE + 1).is_err()); // fee > 100% + assert!(PoolSwapInputs::from_raw(1, 1, 1, 1, 1_000_000, 0).is_err()); // balance ratio < 0.01 + + // No validated swap panics on quote, across a wide value sweep. + let iters = fuzz_iters(200_000); + let mut rng = Rng(0x900d_51ab_c0de_f00d); + let mut validated = 0u64; + for _ in 0..iters { + let bin = rng.in_domain(u128::MAX); + let bout = rng.in_domain(u128::MAX); + let win = 1 + rng.in_domain(1_000); + let wout = 1 + rng.in_domain(1_000); + let ain = rng.in_domain(u128::MAX); + let fee = rng.in_domain(SCALE); + if let Ok(swap) = PoolSwapInputs::from_raw(bin, bout, win, wout, ain, fee) { + validated += 1; + let outcome = std::panic::catch_unwind(|| swap.quote()); + assert!( + outcome.is_ok(), + "checked PoolSwapInputs panicked for bin={bin} bout={bout} win={win} wout={wout} ain={ain} fee={fee}" + ); + } + } + assert!( + validated > 0, + "the pool sweep must exercise at least some in-domain swaps" + ); +} diff --git a/tests/critical_invariants.rs b/tests/critical_invariants.rs new file mode 100644 index 0000000..3e7a64c --- /dev/null +++ b/tests/critical_invariants.rs @@ -0,0 +1,1785 @@ +//! Deterministic assurance checks for the value-sensitive public API. +//! +//! These tests intentionally use no external crates, network services, random +//! seeds, or floating-point reference calculations. The arithmetic oracle is +//! a test-local four-limb model implemented separately from the production +//! arithmetic. Financial tests check structural invariants rather than +//! claiming an external or independently qualified pricing implementation. +//! +//! Residual limits used below: +//! - integer/fixed-point rounding and overflow: exact (`0` raw units); +//! - weighted-pool payout: returned gross output must be no greater than a +//! rigorously rounded-down lower bound on the real integer-exponent output; +//! over the tested domain that lower bound is less than `1.0000004` raw +//! payout units below the real-valued result; +//! - Black-Scholes/Heston/SABR put-call parity: exact against the discounted +//! strike used by the corresponding fixed-point path; +//! - barrier knock-in + knock-out conservation: exact (`0` raw units); +//! - SABR coarse-grid convexity: `1_000` raw units, matching the executable +//! quote guard's documented rounding allowance. + +use solmath::{ + checked_mul_div_ceil_i, checked_mul_div_floor_i, checked_mul_div_i, fp_div, fp_div_i, + fp_div_round, fp_mul, fp_mul_i, fp_mul_i_round, fp_mul_i_round_dw, fp_mul_round, fp_sqrt, + mul_div_ceil, mul_div_ceil_u128, mul_div_floor, mul_div_floor_u128, SolMathError, SCALE, + SCALE_I, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct RefU256([u64; 4]); + +impl RefU256 { + const fn zero() -> Self { + Self([0; 4]) + } + + const fn from_u128(value: u128) -> Self { + Self([value as u64, (value >> 64) as u64, 0, 0]) + } + + fn bit(self, index: usize) -> bool { + ((self.0[index / 64] >> (index % 64)) & 1) != 0 + } + + fn set_bit(&mut self, index: usize) { + self.0[index / 64] |= 1u64 << (index % 64); + } + + fn shl1(&mut self) { + let mut carry = 0u64; + for limb in &mut self.0 { + let next = *limb >> 63; + *limb = (*limb << 1) | carry; + carry = next; + } + } + + fn add_assign(&mut self, rhs: Self) { + let mut carry = false; + for index in 0..4 { + let (sum, carry_a) = self.0[index].overflowing_add(rhs.0[index]); + let (sum, carry_b) = sum.overflowing_add(u64::from(carry)); + self.0[index] = sum; + carry = carry_a || carry_b; + } + assert!(!carry, "reference U256 addition overflowed"); + } + + fn ge(self, rhs: Self) -> bool { + for index in (0..4).rev() { + if self.0[index] != rhs.0[index] { + return self.0[index] > rhs.0[index]; + } + } + true + } + + fn sub_assign(&mut self, rhs: Self) { + assert!(self.ge(rhs)); + let mut borrow = false; + for index in 0..4 { + let (difference, borrow_a) = self.0[index].overflowing_sub(rhs.0[index]); + let (difference, borrow_b) = difference.overflowing_sub(u64::from(borrow)); + self.0[index] = difference; + borrow = borrow_a || borrow_b; + } + assert!(!borrow); + } + + fn low_u128(self) -> u128 { + self.0[0] as u128 | ((self.0[1] as u128) << 64) + } + + fn high_u128_nonzero(self) -> bool { + self.0[2] != 0 || self.0[3] != 0 + } + + fn mul_u128(a: u128, b: u128) -> Self { + let mut product = Self::zero(); + let mut shifted = Self::from_u128(a); + let mut multiplier = b; + while multiplier != 0 { + if multiplier & 1 != 0 { + product.add_assign(shifted); + } + multiplier >>= 1; + if multiplier != 0 { + shifted.shl1(); + } + } + product + } + + fn div_rem_u128(self, divisor: u128) -> (Self, u128) { + assert_ne!(divisor, 0); + let divisor = Self::from_u128(divisor); + let mut quotient = Self::zero(); + let mut remainder = Self::zero(); + for bit in (0..256).rev() { + remainder.shl1(); + if self.bit(bit) { + remainder.0[0] |= 1; + } + if remainder.ge(divisor) { + remainder.sub_assign(divisor); + quotient.set_bit(bit); + } + } + assert!(!remainder.high_u128_nonzero()); + (quotient, remainder.low_u128()) + } +} + +#[derive(Clone, Copy)] +enum UnsignedRounding { + Floor, + Ceil, + NearestHalfUp, +} + +#[derive(Clone, Copy)] +enum SignedRounding { + ToZero, + Floor, + Ceil, + NearestAway, +} + +fn ref_unsigned_mul_div( + a: u128, + b: u128, + divisor: u128, + rounding: UnsignedRounding, +) -> Result { + if divisor == 0 { + return Err(SolMathError::DivisionByZero); + } + let (quotient, remainder) = RefU256::mul_u128(a, b).div_rem_u128(divisor); + if quotient.high_u128_nonzero() { + return Err(SolMathError::Overflow); + } + let mut quotient = quotient.low_u128(); + let increment = match rounding { + UnsignedRounding::Floor => false, + UnsignedRounding::Ceil => remainder != 0, + UnsignedRounding::NearestHalfUp => remainder >= divisor - remainder, + }; + if increment { + quotient = quotient.checked_add(1).ok_or(SolMathError::Overflow)?; + } + Ok(quotient) +} + +fn signed_from_magnitude(magnitude: u128, negative: bool) -> Result { + if negative { + if magnitude == 1u128 << 127 { + Ok(i128::MIN) + } else if magnitude < 1u128 << 127 { + Ok(-(magnitude as i128)) + } else { + Err(SolMathError::Overflow) + } + } else if magnitude <= i128::MAX as u128 { + Ok(magnitude as i128) + } else { + Err(SolMathError::Overflow) + } +} + +fn ref_signed_mul_div( + a: i128, + b: i128, + divisor: i128, + rounding: SignedRounding, +) -> Result { + if divisor == 0 { + return Err(SolMathError::DivisionByZero); + } + let negative = (a < 0) ^ (b < 0) ^ (divisor < 0); + let unsigned_divisor = divisor.unsigned_abs(); + let (quotient, remainder) = + RefU256::mul_u128(a.unsigned_abs(), b.unsigned_abs()).div_rem_u128(unsigned_divisor); + if quotient.high_u128_nonzero() { + return Err(SolMathError::Overflow); + } + let mut magnitude = quotient.low_u128(); + let increment = if remainder == 0 { + false + } else { + match rounding { + SignedRounding::ToZero => false, + SignedRounding::Floor => negative, + SignedRounding::Ceil => !negative, + SignedRounding::NearestAway => remainder >= unsigned_divisor - remainder, + } + }; + if increment { + magnitude = magnitude.checked_add(1).ok_or(SolMathError::Overflow)?; + } + signed_from_magnitude(magnitude, negative) +} + +fn ref_double_word(a: i128, b: i128) -> Result<(i128, i128), SolMathError> { + let negative = (a < 0) ^ (b < 0); + let (quotient, remainder) = + RefU256::mul_u128(a.unsigned_abs(), b.unsigned_abs()).div_rem_u128(SCALE); + if quotient.high_u128_nonzero() { + return Err(SolMathError::Overflow); + } + let mut magnitude = quotient.low_u128(); + let rounded_up = remainder >= SCALE / 2; + let residual_magnitude = if rounded_up { + magnitude = magnitude.checked_add(1).ok_or(SolMathError::Overflow)?; + remainder as i128 - SCALE_I + } else { + remainder as i128 + }; + let high = signed_from_magnitude(magnitude, negative)?; + let low = if negative { + residual_magnitude + .checked_neg() + .ok_or(SolMathError::Overflow)? + } else { + residual_magnitude + }; + if high == i128::MIN && low < 0 { + return Err(SolMathError::Overflow); + } + Ok((high, low)) +} + +fn unsigned_edges() -> [u128; 16] { + [ + 0, + 1, + 2, + SCALE / 2 - 1, + SCALE / 2, + SCALE / 2 + 1, + SCALE - 1, + SCALE, + SCALE + 1, + 2 * SCALE, + 1u128 << 63, + 1u128 << 64, + u128::MAX / SCALE, + u128::MAX / 2, + u128::MAX - 1, + u128::MAX, + ] +} + +fn signed_edges() -> [i128; 17] { + [ + i128::MIN, + i128::MIN + 1, + -2 * SCALE_I, + -SCALE_I - 1, + -SCALE_I, + -SCALE_I / 2, + -2, + -1, + 0, + 1, + 2, + SCALE_I / 2, + SCALE_I, + SCALE_I + 1, + 2 * SCALE_I, + i128::MAX - 1, + i128::MAX, + ] +} + +#[test] +fn reference_u256_model_self_checks_against_native_arithmetic() { + for a in 0u128..64 { + for b in 0u128..64 { + for divisor in 1u128..32 { + let product = a * b; + let (quotient, remainder) = RefU256::mul_u128(a, b).div_rem_u128(divisor); + assert!(!quotient.high_u128_nonzero()); + assert_eq!(quotient.low_u128(), product / divisor); + assert_eq!(remainder, product % divisor); + } + } + } + let (quotient, remainder) = RefU256::mul_u128(u128::MAX, u128::MAX).div_rem_u128(u128::MAX); + assert_eq!(quotient.low_u128(), u128::MAX); + assert!(!quotient.high_u128_nonzero()); + assert_eq!(remainder, 0); +} + +fn assert_exact_fixed_point_sqrt_floor(x: u128) { + let result = fp_sqrt(x).unwrap(); + let radicand = RefU256::mul_u128(x, SCALE); + let square = RefU256::mul_u128(result, result); + let successor = result.checked_add(1).unwrap(); + let successor_square = RefU256::mul_u128(successor, successor); + assert!( + radicand.ge(square), + "fp_sqrt({x})={result} is above the exact floor" + ); + assert!( + !radicand.ge(successor_square), + "fp_sqrt({x})={result} is below the exact floor" + ); +} + +#[test] +fn fixed_point_sqrt_is_the_exact_floor_across_full_width() { + for x in 0..=u16::MAX as u128 { + assert_exact_fixed_point_sqrt_floor(x); + } + + // Deterministic, evenly spaced coverage across the complete u128 domain, + // including the path where `x * SCALE` requires 256 bits. + let step = u128::MAX / 4_096; + for index in 0u128..=4_096 { + assert_exact_fixed_point_sqrt_floor(step * index); + } + for x in unsigned_edges() { + assert_exact_fixed_point_sqrt_floor(x); + } +} + +#[test] +fn fixed_point_rounding_matches_exact_256_bit_model() { + let unsigned = unsigned_edges(); + for &a in &unsigned { + for &b in &unsigned { + assert_eq!( + fp_mul(a, b), + ref_unsigned_mul_div(a, b, SCALE, UnsignedRounding::Floor), + "fp_mul({a}, {b})" + ); + assert_eq!( + fp_mul_round(a, b), + ref_unsigned_mul_div(a, b, SCALE, UnsignedRounding::NearestHalfUp), + "fp_mul_round({a}, {b})" + ); + assert_eq!( + fp_div(a, b), + ref_unsigned_mul_div(a, SCALE, b, UnsignedRounding::Floor), + "fp_div({a}, {b})" + ); + assert_eq!( + fp_div_round(a, b), + ref_unsigned_mul_div(a, SCALE, b, UnsignedRounding::NearestHalfUp), + "fp_div_round({a}, {b})" + ); + } + } + + let signed = signed_edges(); + for &a in &signed { + for &b in &signed { + assert_eq!( + fp_mul_i(a, b), + ref_signed_mul_div(a, b, SCALE_I, SignedRounding::ToZero), + "fp_mul_i({a}, {b})" + ); + assert_eq!( + fp_mul_i_round(a, b), + ref_signed_mul_div(a, b, SCALE_I, SignedRounding::NearestAway), + "fp_mul_i_round({a}, {b})" + ); + assert_eq!( + fp_div_i(a, b), + ref_signed_mul_div(a, SCALE_I, b, SignedRounding::ToZero), + "fp_div_i({a}, {b})" + ); + let expected_dw = ref_double_word(a, b); + let actual_dw = fp_mul_i_round_dw(a, b).map(|value| (value.hi(), value.lo())); + assert_eq!(actual_dw, expected_dw, "fp_mul_i_round_dw({a}, {b})"); + } + } +} + +#[test] +fn raw_mul_div_and_signed_rounding_match_exact_256_bit_model() { + let values = unsigned_edges(); + let divisors = [ + 0, + 1, + 2, + 3, + SCALE - 1, + SCALE, + SCALE + 1, + 1u128 << 64, + u128::MAX, + ]; + for &a in &values { + for &b in &values { + for &divisor in &divisors { + assert_eq!( + mul_div_floor_u128(a, b, divisor), + ref_unsigned_mul_div(a, b, divisor, UnsignedRounding::Floor), + "mul_div_floor_u128({a}, {b}, {divisor})" + ); + assert_eq!( + mul_div_ceil_u128(a, b, divisor), + ref_unsigned_mul_div(a, b, divisor, UnsignedRounding::Ceil), + "mul_div_ceil_u128({a}, {b}, {divisor})" + ); + } + } + } + + let signed = signed_edges(); + let signed_divisors = [ + i128::MIN, + -SCALE_I, + -3, + -2, + -1, + 0, + 1, + 2, + 3, + SCALE_I, + i128::MAX, + ]; + for &a in &signed { + for &b in &signed { + for &divisor in &signed_divisors { + assert_eq!( + checked_mul_div_i(a, b, divisor), + ref_signed_mul_div(a, b, divisor, SignedRounding::ToZero), + "checked_mul_div_i({a}, {b}, {divisor})" + ); + assert_eq!( + checked_mul_div_floor_i(a, b, divisor), + ref_signed_mul_div(a, b, divisor, SignedRounding::Floor), + "checked_mul_div_floor_i({a}, {b}, {divisor})" + ); + assert_eq!( + checked_mul_div_ceil_i(a, b, divisor), + ref_signed_mul_div(a, b, divisor, SignedRounding::Ceil), + "checked_mul_div_ceil_i({a}, {b}, {divisor})" + ); + } + } + } +} + +#[test] +fn u64_mul_div_matches_native_widened_model() { + let values = [ + 0, + 1, + 2, + 3, + u32::MAX as u64, + 1u64 << 32, + u64::MAX - 1, + u64::MAX, + ]; + let divisors = [0, 1, 2, 3, u32::MAX as u64, 1u64 << 32, u64::MAX]; + for &a in &values { + for &b in &values { + for &divisor in &divisors { + let expected_floor = if divisor == 0 { + Err(SolMathError::DivisionByZero) + } else { + let quotient = (a as u128 * b as u128) / divisor as u128; + u64::try_from(quotient).map_err(|_| SolMathError::Overflow) + }; + let expected_ceil = if divisor == 0 { + Err(SolMathError::DivisionByZero) + } else { + let product = a as u128 * b as u128; + let quotient = + product / divisor as u128 + u128::from(product % divisor as u128 != 0); + u64::try_from(quotient).map_err(|_| SolMathError::Overflow) + }; + assert_eq!(mul_div_floor(a, b, divisor), expected_floor); + assert_eq!(mul_div_ceil(a, b, divisor), expected_ceil); + } + } + } +} + +#[test] +fn exact_half_ties_and_signed_extrema_are_explicit() { + assert_eq!(fp_mul_round(1, SCALE / 2), Ok(1)); + assert_eq!(fp_mul_i_round(1, SCALE_I / 2), Ok(1)); + assert_eq!(fp_mul_i_round(-1, SCALE_I / 2), Ok(-1)); + assert_eq!(fp_div_round(1, 2 * SCALE), Ok(1)); + assert_eq!(fp_div_round(1, 3), Ok(SCALE / 3)); + + assert_eq!(checked_mul_div_i(i128::MIN, 1, 1), Ok(i128::MIN)); + assert_eq!( + checked_mul_div_i(i128::MIN, -1, 1), + Err(SolMathError::Overflow) + ); + assert_eq!(checked_mul_div_i(i128::MIN, -1, -1), Ok(i128::MIN)); + assert_eq!(checked_mul_div_floor_i(-1, 1, 2), Ok(-1)); + assert_eq!(checked_mul_div_ceil_i(-1, 1, 2), Ok(0)); + assert_eq!(checked_mul_div_floor_i(1, 1, -2), Ok(-1)); + assert_eq!(checked_mul_div_ceil_i(1, 1, -2), Ok(0)); +} + +#[cfg(feature = "pool")] +mod pool_invariants { + use super::*; + use solmath::weighted_pool_swap; + + const Q24: u128 = 1_000_000_000_000_000_000_000_000; + + fn ceil_mul_div(a: u128, b: u128, divisor: u128) -> u128 { + ref_unsigned_mul_div(a, b, divisor, UnsignedRounding::Ceil).unwrap() + } + + fn conservative_integer_power_gross_lower_bound( + balance_in: u128, + balance_out: u128, + amount_in: u128, + exponent: u128, + ) -> u128 { + let denominator = balance_in + amount_in; + let ratio_high = ceil_mul_div(balance_in, Q24, denominator); + let mut power_high = Q24; + for _ in 0..exponent { + power_high = ceil_mul_div(power_high, ratio_high, Q24).min(Q24); + } + ref_unsigned_mul_div(balance_out, Q24 - power_high, Q24, UnsignedRounding::Floor).unwrap() + } + + #[test] + fn pool_payout_is_protocol_favouring_on_integer_weight_ratios() { + let balances_in = [SCALE, 10 * SCALE, 1_000 * SCALE]; + let balances_out = [SCALE, 7 * SCALE, 10_000 * SCALE]; + let exponents = [1u128, 2, 3, 5, 10, 20]; + let fee_rates = [0, 1, SCALE / 1_000, SCALE / 2, SCALE]; + + for &balance_in in &balances_in { + let amounts = [ + 1, + SCALE / 1_000, + SCALE / 10, + SCALE, + 10 * SCALE, + 99 * balance_in, + ]; + for &balance_out in &balances_out { + for &amount_in in &amounts { + for &exponent in &exponents { + for &fee_rate in &fee_rates { + let (net_out, fee) = weighted_pool_swap( + balance_in, + balance_out, + exponent * SCALE, + SCALE, + amount_in, + fee_rate, + ) + .unwrap_or_else(|error| { + panic!( + "certified pool input failed: bi={balance_in}, bo={balance_out}, \ + amount={amount_in}, exponent={exponent}, fee={fee_rate}: {error:?}" + ) + }); + let gross = net_out.checked_add(fee).unwrap(); + let exact_lower = conservative_integer_power_gross_lower_bound( + balance_in, + balance_out, + amount_in, + exponent, + ); + assert!( + gross <= exact_lower, + "trader-favouring payout: gross={gross}, exact_lower={exact_lower}, \ + bi={balance_in}, bo={balance_out}, amount={amount_in}, exponent={exponent}" + ); + assert!(gross < balance_out, "pool reserve was drained"); + let exact_fee = ref_unsigned_mul_div( + gross, + fee_rate, + SCALE, + UnsignedRounding::Ceil, + ) + .unwrap(); + assert_eq!(fee, exact_fee, "fee must round toward the protocol"); + assert_eq!(net_out, gross - fee); + } + } + } + } + } + } + + #[test] + fn pool_rejects_shapes_outside_the_certified_domain() { + assert_eq!( + weighted_pool_swap(SCALE, SCALE, SCALE, SCALE, 99 * SCALE + 1, 0), + Err(SolMathError::DomainError) + ); + assert_eq!( + weighted_pool_swap(SCALE, SCALE, 20 * SCALE + 1, SCALE, SCALE, 0), + Err(SolMathError::DomainError) + ); + assert_eq!( + weighted_pool_swap(u128::MAX, SCALE, SCALE, SCALE, 1, 0), + Err(SolMathError::Overflow) + ); + assert_eq!( + weighted_pool_swap(SCALE, SCALE, 0, SCALE, SCALE, 0), + Err(SolMathError::DomainError) + ); + assert_eq!( + weighted_pool_swap(SCALE, SCALE, SCALE, 0, SCALE, 0), + Err(SolMathError::DivisionByZero) + ); + } +} + +#[cfg(feature = "bs")] +mod black_scholes_invariants { + use super::*; + use solmath::{ + black_scholes_price, black_scholes_price_hp, bs_full, bs_full_hp, exp_fixed_hp, + exp_fixed_i, fp_mul_hp_i, + }; + + const HP_FACTOR: i128 = 1_000; + + fn standard_discounted_strike(k: u128, r: u128, t: u128) -> u128 { + let rt = fp_mul_i(r as i128, t as i128).unwrap(); + fp_mul_i(k as i128, exp_fixed_i(-rt).unwrap()).unwrap() as u128 + } + + fn hp_discounted_strike(k: u128, r: u128, t: u128) -> u128 { + let r_hp = r as i128 * HP_FACTOR; + let t_hp = t as i128 * HP_FACTOR; + let k_hp = k as i128 * HP_FACTOR; + let rt_hp = fp_mul_hp_i(r_hp, t_hp).unwrap(); + let discount_hp = exp_fixed_hp(-rt_hp).unwrap(); + let strike_hp = fp_mul_hp_i(k_hp, discount_hp).unwrap(); + ((strike_hp + HP_FACTOR / 2) / HP_FACTOR) as u128 + } + + fn assert_bounds_and_parity( + label: &str, + call: u128, + put: u128, + s: u128, + k: u128, + discounted_strike: u128, + ) { + assert!(call <= s, "{label}: call exceeds spot"); + assert!(put <= k, "{label}: put exceeds undiscounted strike"); + assert!( + call >= s.saturating_sub(discounted_strike), + "{label}: call below lower bound" + ); + assert!( + put >= discounted_strike.saturating_sub(s), + "{label}: put below lower bound" + ); + let call_side = call.checked_add(discounted_strike).unwrap(); + let put_side = put.checked_add(s).unwrap(); + assert_eq!( + call_side, put_side, + "{label}: put-call parity residual must be zero" + ); + } + + #[test] + fn price_paths_obey_hard_bounds_and_exact_parity() { + let spots = [SCALE, 25 * SCALE, 100 * SCALE, 400 * SCALE, 1_000 * SCALE]; + let strikes = [SCALE, 20 * SCALE, 100 * SCALE, 500 * SCALE, 1_000 * SCALE]; + let rates = [0, SCALE / 100, SCALE / 20, SCALE / 5]; + let sigmas = [SCALE / 100, SCALE / 10, SCALE / 2, 2 * SCALE]; + let times = [SCALE / 1_000, SCALE / 10, SCALE, 5 * SCALE]; + + for &s in &spots { + for &k in &strikes { + for &r in &rates { + for &sigma in &sigmas { + for &t in × { + let standard = black_scholes_price(s, k, r, sigma, t).unwrap(); + let standard_full = bs_full(s, k, r, sigma, t).unwrap(); + assert_eq!(standard, (standard_full.call, standard_full.put)); + assert_bounds_and_parity( + "standard BS", + standard.0, + standard.1, + s, + k, + standard_discounted_strike(k, r, t), + ); + assert!((0..=SCALE_I).contains(&standard_full.call_delta)); + assert!((-SCALE_I..=0).contains(&standard_full.put_delta)); + assert!(standard_full.gamma >= 0); + assert!(standard_full.vega >= 0); + + let hp = black_scholes_price_hp(s, k, r, sigma, t).unwrap(); + let hp_full = bs_full_hp(s, k, r, sigma, t).unwrap(); + assert_eq!(hp, (hp_full.call, hp_full.put)); + assert_bounds_and_parity( + "HP BS", + hp.0, + hp.1, + s, + k, + hp_discounted_strike(k, r, t), + ); + assert!((0..=SCALE_I).contains(&hp_full.call_delta)); + assert!((-SCALE_I..=0).contains(&hp_full.put_delta)); + assert!(hp_full.gamma >= 0); + assert!(hp_full.vega >= 0); + } + } + } + } + } + } + + #[test] + fn extreme_public_inputs_fail_closed_without_panicking() { + use solmath::{bs_rho, bs_theta}; + + let maximum = i128::MAX as u128; + let cases = [ + (u128::MAX, SCALE, 0, SCALE / 5, SCALE), + (SCALE, u128::MAX, 0, SCALE / 5, SCALE), + (SCALE, SCALE, u128::MAX, SCALE / 5, SCALE), + (SCALE, SCALE, 0, u128::MAX, SCALE), + (SCALE, SCALE, 0, SCALE / 5, u128::MAX), + (maximum, SCALE, 0, SCALE / 5, SCALE), + (SCALE, maximum, 0, SCALE / 5, SCALE), + (SCALE, SCALE, maximum, SCALE / 5, SCALE), + (SCALE, SCALE, 0, maximum, SCALE), + (SCALE, SCALE, 0, SCALE / 5, maximum), + // Both spot AND strike at the i128 ceiling simultaneously: the + // discounted-strike Greek terms (theta/rho) can each approach the + // i128 limits, and their combination once overflowed. Regression for + // the bs_full / bs_theta subtract-with-overflow panic. + (maximum, maximum, SCALE, SCALE / 2, SCALE / 100), + (maximum, maximum, SCALE, SCALE / 5, SCALE), + (maximum, maximum, maximum, maximum, maximum), + ]; + + for &(s, k, r, sigma, t) in &cases { + for (label, high_precision) in [("standard", false), ("high precision", true)] { + let outcome = std::panic::catch_unwind(|| { + if high_precision { + black_scholes_price_hp(s, k, r, sigma, t) + } else { + black_scholes_price(s, k, r, sigma, t) + } + }); + assert!( + outcome.is_ok(), + "{label} BS panicked for s={s}, k={k}, r={r}, sigma={sigma}, t={t}" + ); + if let Ok((call, put)) = outcome.unwrap() { + assert!(call <= s); + assert!(put <= k); + } + } + + // The full-Greek surface (theta/rho especially) must fail closed too. + for (label, outcome) in [ + ( + "bs_full", + std::panic::catch_unwind(|| bs_full(s, k, r, sigma, t)).map(|_| ()), + ), + ( + "bs_theta", + std::panic::catch_unwind(|| bs_theta(s, k, r, sigma, t)).map(|_| ()), + ), + ( + "bs_rho", + std::panic::catch_unwind(|| bs_rho(s, k, r, sigma, t)).map(|_| ()), + ), + ] { + assert!( + outcome.is_ok(), + "{label} panicked for s={s}, k={k}, r={r}, sigma={sigma}, t={t}" + ); + } + } + } +} + +/// No-panic guarantee across the realistic mid-range input space. +/// +/// The prior `extreme_*` sweeps only probed input *boundaries* (a single field +/// at `i128::MAX`). The overflow panics fixed here lived in the *interior*: +/// `implied_vol` on a plausible deep-OTM long-dated quote, and `bs_theta` when +/// spot and strike are both large together. This deterministic fuzz walks the +/// realistic financial ranges (including the long maturities and lopsided +/// spot/strike ratios that trigger the degenerate solver bracket) and asserts +/// every public pricing entry point returns a `Result` rather than panicking. +#[cfg(feature = "iv")] +mod pricing_no_panic_fuzz { + use super::*; + use solmath::{black_scholes_price, bs_full, bs_rho, bs_theta, bs_vega, implied_vol}; + + // xorshift64 — deterministic, dependency-free, reproducible in CI. + struct Rng(u64); + impl Rng { + fn next(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + // A value biased toward realistic finance ranges, with occasional + // extremes. The near-cap band (~1e17) is emphasised because that is + // where near-ATM volga blows up and the IV Halley step overflowed. + fn amount(&mut self) -> u128 { + match self.next() % 16 { + 0 => 0, + 1 => 1, + 2 => SCALE, + 3 => 100_000 * SCALE, // the IV price cap + 4 => 100_001 * SCALE, // just over the cap + 5 => i128::MAX as u128, + 6 => SCALE / 100, // 0.01 + 7 => (self.next() as u128) % SCALE, // fractional + 8 => 99_999 * SCALE, // just inside the cap (near-ATM volga) + 9 => 99_998 * SCALE, + 10 => 100_000 * SCALE - 1 - (self.next() as u128) % SCALE, + _ => SCALE * (1 + (self.next() as u128) % 200_000), // 1 .. 200k units + } + } + fn rate(&mut self) -> u128 { + (self.next() as u128) % (SCALE / 2) // 0 .. 50% + } + fn sigma(&mut self) -> u128 { + 1 + (self.next() as u128) % (5 * SCALE) // ~0 .. 500% vol + } + fn time(&mut self) -> u128 { + // Bias toward both extremes: tiny maturities (where near-ATM volga + // diverges) and long ones (where sigma*sqrt(T) grows). Both were + // overflow triggers in the IV solver. + match self.next() % 6 { + 0 => 1, + 1 => SCALE / 10_000, + 2 => 20 * SCALE, + 3 => 30 * SCALE, + _ => 1 + (self.next() as u128) % (30 * SCALE), + } + } + } + + #[test] + fn implied_vol_degenerate_boundary_is_fail_closed_not_panic() { + // Exact regression vector: deep-OTM (strike 100x spot), 20-year maturity, + // near-zero rate. Formerly panicked via an unchecked square in + // `normalised_vega` once the solver bracket ran to the i128::MAX/2 + // sentinel. Must now return an Err, not unwind. + let outcome = std::panic::catch_unwind(|| { + implied_vol( + 10_000_000_000, // market price 0.01 + 10_000_000_000, // spot 0.01 + 1_000_000_000_000, // strike 1.0 + 1, // rate ~0 + 20_000_000_000_000, // t = 20 years + ) + }); + assert!( + outcome.is_ok(), + "implied_vol panicked on the degenerate boundary vector" + ); + assert!( + matches!( + outcome.unwrap(), + Err(SolMathError::NoConvergence) | Err(SolMathError::Overflow) + ), + "degenerate implied_vol should fail closed" + ); + } + + #[test] + fn implied_vol_near_atm_huge_price_does_not_overflow_halley_step() { + // Near-ATM (s ≈ k ≈ 1e17, the price cap), essentially-zero maturity: + // volga diverges and the Halley step's `f * volga` product formerly + // overflowed i128 in `halley_step_bracketed`. The routine must now fall + // back to bisection and return a Result, never unwind. Under debug + // assertions this also exercises the `mul_fast` precondition guard. + let cases = [ + (99_999 * SCALE, 99_999 * SCALE, 100_000 * SCALE, 0, 1), + ( + 100_000 * SCALE, + 100_000 * SCALE, + 99_999 * SCALE, + 500_000_000_000, + 1, + ), + (99_999 * SCALE, 100_000 * SCALE, 99_999 * SCALE, 1, 1), + ]; + for (mp, s, k, r, t) in cases { + let outcome = std::panic::catch_unwind(|| implied_vol(mp, s, k, r, t)); + assert!( + outcome.is_ok(), + "implied_vol panicked for mp={mp} s={s} k={k} r={r} t={t}" + ); + } + } + + #[test] + fn pricing_surface_never_panics_over_realistic_space() { + let mut rng = Rng(0x5eed_1234_9e37_79b9); + // 150k draws keeps the default test well under a second while covering + // the interior space the boundary sweeps miss. CI's soak step raises + // this via SOLMATH_FUZZ_ITERS for a much deeper sweep. + let iters: u64 = std::env::var("SOLMATH_FUZZ_ITERS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(150_000); + for _ in 0..iters { + let s = rng.amount(); + let k = rng.amount(); + let r = rng.rate(); + let sigma = rng.sigma(); + let t = rng.time(); + let mp = rng.amount(); + + macro_rules! no_panic { + ($label:literal, $call:expr) => {{ + let outcome = std::panic::catch_unwind(|| $call); + assert!( + outcome.is_ok(), + concat!($label, " panicked for s={} k={} r={} sigma={} t={} mp={}"), + s, + k, + r, + sigma, + t, + mp + ); + }}; + } + + no_panic!( + "black_scholes_price", + black_scholes_price(s, k, r, sigma, t) + ); + no_panic!("bs_full", bs_full(s, k, r, sigma, t)); + no_panic!("bs_theta", bs_theta(s, k, r, sigma, t)); + no_panic!("bs_rho", bs_rho(s, k, r, sigma, t)); + no_panic!("bs_vega", bs_vega(s, k, r, sigma, t)); + no_panic!("implied_vol", implied_vol(mp, s, k, r, t)); + } + } +} + +#[cfg(feature = "barrier")] +mod barrier_invariants { + use super::*; + use solmath::{barrier_option, barrier_option_with_state, BarrierType}; + + fn pair(kind_out: BarrierType) -> BarrierType { + match kind_out { + BarrierType::DownAndOut => BarrierType::DownAndIn, + BarrierType::UpAndOut => BarrierType::UpAndIn, + _ => unreachable!(), + } + } + + #[test] + fn knock_in_plus_knock_out_equals_vanilla_exactly() { + let spots = [50 * SCALE, 100 * SCALE]; + let rates = [0, SCALE / 20]; + let sigmas = [SCALE / 10, SCALE / 2]; + let times = [SCALE / 4, SCALE]; + + for &s in &spots { + let strikes = [s / 2, s, 3 * s / 2]; + let down_barriers = [s / 2, 3 * s / 4, s - SCALE]; + let up_barriers = [s + SCALE, 5 * s / 4, 3 * s / 2]; + for &(kind_out, barriers) in &[ + (BarrierType::DownAndOut, down_barriers), + (BarrierType::UpAndOut, up_barriers), + ] { + for &h in &barriers { + for &k in &strikes { + for &r in &rates { + for &sigma in &sigmas { + for &t in × { + for is_call in [false, true] { + let out = + barrier_option(s, k, h, r, sigma, t, is_call, kind_out) + .unwrap(); + let knocked_in = barrier_option( + s, + k, + h, + r, + sigma, + t, + is_call, + pair(kind_out), + ) + .unwrap(); + assert_eq!(out.vanilla, knocked_in.vanilla); + assert_eq!(out.price + knocked_in.price, out.vanilla); + assert!(out.price <= out.vanilla); + assert!(knocked_in.price <= knocked_in.vanilla); + + let historical_out = barrier_option_with_state( + s, k, h, r, sigma, t, is_call, kind_out, true, + ) + .unwrap(); + let historical_in = barrier_option_with_state( + s, + k, + h, + r, + sigma, + t, + is_call, + pair(kind_out), + true, + ) + .unwrap(); + assert_eq!(historical_out.price, 0); + assert_eq!(historical_in.price, historical_in.vanilla); + assert_eq!(historical_out.vanilla, historical_in.vanilla); + } + } + } + } + } + } + } + } + } + + #[test] + fn extreme_barrier_inputs_fail_closed_without_panicking() { + let maximum_hp_input = (i128::MAX / 1_000) as u128; + let cases = [ + (u128::MAX, SCALE, SCALE / 2, 0, SCALE / 5, SCALE), + (SCALE, u128::MAX, SCALE / 2, 0, SCALE / 5, SCALE), + (SCALE, SCALE, u128::MAX, 0, SCALE / 5, SCALE), + (SCALE, SCALE, SCALE / 2, u128::MAX, SCALE / 5, SCALE), + (SCALE, SCALE, SCALE / 2, 0, u128::MAX, SCALE), + (SCALE, SCALE, SCALE / 2, 0, SCALE / 5, u128::MAX), + ( + maximum_hp_input, + maximum_hp_input, + maximum_hp_input / 2, + 0, + SCALE, + SCALE, + ), + ( + maximum_hp_input, + maximum_hp_input / 2, + maximum_hp_input - 1, + SCALE, + SCALE, + SCALE, + ), + (1, maximum_hp_input, 2, SCALE, SCALE, SCALE), + ]; + + for &(s, k, h, r, sigma, t) in &cases { + for &is_call in &[false, true] { + for barrier_type in [ + BarrierType::DownAndOut, + BarrierType::DownAndIn, + BarrierType::UpAndOut, + BarrierType::UpAndIn, + ] { + let outcome = std::panic::catch_unwind(|| { + barrier_option(s, k, h, r, sigma, t, is_call, barrier_type) + }); + assert!( + outcome.is_ok(), + "barrier panicked for s={s}, k={k}, h={h}, r={r}, sigma={sigma}, \ + t={t}, call={is_call}, type={barrier_type:?}" + ); + if let Ok(result) = outcome.unwrap() { + assert!(result.price <= result.vanilla); + } + } + } + } + } +} + +#[cfg(feature = "heston")] +mod heston_invariants { + use super::*; + use solmath::{exp_fixed_i, heston_price}; + + fn discounted_strike(k: u128, r: u128, t: u128) -> u128 { + let rt = fp_mul_i(r as i128, t as i128).unwrap(); + fp_mul_i(k as i128, exp_fixed_i(-rt).unwrap()).unwrap() as u128 + } + + #[test] + fn deterministic_heston_obeys_bounds_and_stochastic_path_fails_closed() { + let spots = [SCALE, 25 * SCALE, 100 * SCALE, 1_000 * SCALE]; + let strikes = [SCALE, 20 * SCALE, 100 * SCALE, 1_000 * SCALE]; + let rates = [0, SCALE / 20, SCALE / 5]; + let times = [SCALE / 100, SCALE, 10 * SCALE]; + let variances = [0, SCALE / 10_000, SCALE / 25, SCALE / 4]; + let kappas = [0, SCALE / 10, SCALE, 20 * SCALE]; + + for &s in &spots { + for &k in &strikes { + for &r in &rates { + for &t in × { + for &v0 in &variances { + for &kappa in &kappas { + for &theta in &variances { + let (call, put) = + heston_price(s, k, r, t, v0, kappa, theta, 0, 0).unwrap(); + let kd = discounted_strike(k, r, t); + assert!(call <= s); + assert!(put <= k); + assert!(call >= s.saturating_sub(kd)); + assert!(put >= kd.saturating_sub(s)); + assert_eq!(call + kd, put + s); + } + } + } + } + } + } + } + + for xi in [1, SCALE / 1_000, SCALE / 2, 5 * SCALE] { + assert_eq!( + heston_price( + 100 * SCALE, + 100 * SCALE, + SCALE / 20, + SCALE, + SCALE / 25, + SCALE, + SCALE / 25, + xi, + -SCALE_I / 2, + ), + Err(SolMathError::NoConvergence) + ); + } + } +} + +#[cfg(feature = "nig")] +mod nig_invariants { + use super::*; + use solmath::{nig_call_64, nig_call_price, nig_price_certified, nig_put_64, NigParams}; + + struct NigRng(u64); + + impl NigRng { + fn next_u128(&mut self) -> u128 { + let mut next = || { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + }; + next() as u128 | ((next() as u128) << 64) + } + } + + #[test] + fn positive_expiry_nig_obeys_bounds_and_parity() { + let params = NigParams { + alpha: 10 * SCALE, + beta: -2 * SCALE_I, + delta_per_year: SCALE / 5, + }; + let quote = nig_price_certified( + 100 * SCALE, + 100 * SCALE, + SCALE_I / 20, + 0, + SCALE, + params, + 5_000_000_000, + ) + .unwrap(); + assert!(quote.call <= 100 * SCALE + quote.max_abs_error); + assert!(quote.put <= 100 * SCALE + quote.max_abs_error); + let discounted_strike = 95_122_942_450_071u128; + assert!( + quote + .call + .abs_diff(quote.put + (100 * SCALE - discounted_strike)) + <= 64, + "quote={quote:?}" + ); + + let call12 = nig_call_price( + 100 * SCALE, + 100 * SCALE, + SCALE / 20, + SCALE, + 10 * SCALE, + -2 * SCALE_I, + SCALE / 5, + ) + .unwrap(); + let call6 = nig_call_64( + 100_000_000, + 100_000_000, + 50_000, + 1_000_000, + 10_000_000, + -2_000_000, + 200_000, + ) + .unwrap(); + let put6 = nig_put_64( + 100_000_000, + 100_000_000, + 50_000, + 1_000_000, + 10_000_000, + -2_000_000, + 200_000, + ) + .unwrap(); + assert!(call6 > 0 && put6 > 0); + assert!(call12.abs_diff((call6 as u128) * 1_000_000) <= 500_000); + + assert_eq!( + nig_call_price(100 * SCALE, 100 * SCALE, 0, SCALE, SCALE, 0, SCALE), + Err(SolMathError::DomainError) + ); + assert_eq!( + nig_call_price(100 * SCALE, 90 * SCALE, 0, 0, 10 * SCALE, 0, SCALE), + Ok(10 * SCALE) + ); + assert_eq!( + nig_call_64(100_000_000, 90_000_000, 0, 0, 10_000_000, 0, 1_000_000), + Ok(10_000_000) + ); + assert_eq!( + nig_put_64(90_000_000, 100_000_000, 0, 0, 10_000_000, 0, 1_000_000), + Ok(10_000_000) + ); + } + + #[test] + fn full_width_nig_inputs_fail_closed_without_panicking() { + let cases = [ + ( + u128::MAX, + u128::MAX, + i128::MIN, + i128::MAX, + u128::MAX, + NigParams { + alpha: u128::MAX, + beta: i128::MIN, + delta_per_year: u128::MAX, + }, + ), + ( + 100 * SCALE, + 100 * SCALE, + 0, + 0, + SCALE, + NigParams { + alpha: 10 * SCALE, + beta: i128::MIN, + delta_per_year: SCALE, + }, + ), + ( + 100 * SCALE, + 100 * SCALE, + i128::MAX, + i128::MIN, + SCALE, + NigParams { + alpha: 10 * SCALE, + beta: 0, + delta_per_year: SCALE, + }, + ), + ]; + for (spot, strike, rate, dividend, time, params) in cases { + let result = std::panic::catch_unwind(|| { + nig_price_certified(spot, strike, rate, dividend, time, params, u128::MAX) + }); + assert!(result.is_ok(), "NIG panicked for {params:?}"); + assert!(result.unwrap().is_err()); + } + + let iters: u64 = std::env::var("SOLMATH_FUZZ_ITERS") + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(100_000); + let mut rng = NigRng(0x4e49_475f_6675_7a7a); + for index in 0..iters { + let (spot, strike, rate, dividend, time, params, requested) = if index % 1_024 == 0 { + // Periodically force execution past the cheap domain gates so + // the fixed-point density and quadrature paths are included + // in the no-panic soak as well as arbitrary full-width input. + let spot = 1 + rng.next_u128() % (100_000 * SCALE); + let strike = 1 + rng.next_u128() % (100_000 * SCALE); + let rate = (rng.next_u128() % (SCALE / 2 + 1)) as i128 - SCALE_I / 4; + let dividend = (rng.next_u128() % (SCALE / 2 + 1)) as i128 - SCALE_I / 4; + let days = 1 + rng.next_u128() % 1_825; + let time = days * SCALE / 365; + let params = NigParams { + alpha: 2 * SCALE + rng.next_u128() % (98 * SCALE + 1), + beta: -SCALE_I / 2, + delta_per_year: SCALE / 5 + rng.next_u128() % SCALE, + }; + let requested = spot.max(strike) / 1_000 + 1; + (spot, strike, rate, dividend, time, params, requested) + } else { + ( + rng.next_u128(), + rng.next_u128(), + rng.next_u128() as i128, + rng.next_u128() as i128, + rng.next_u128(), + NigParams { + alpha: rng.next_u128(), + beta: rng.next_u128() as i128, + delta_per_year: rng.next_u128(), + }, + rng.next_u128(), + ) + }; + let _ = nig_price_certified(spot, strike, rate, dividend, time, params, requested); + } + } +} + +#[cfg(feature = "american-kbi")] +mod american_kbi_invariants { + use super::*; + use solmath::{american_kbi_price, AmericanKbiKind}; + + struct Rng(u64); + + impl Rng { + fn next_u128(&mut self) -> u128 { + let mut next = || { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + }; + (u128::from(next()) << 64) | u128::from(next()) + } + } + + #[test] + fn accepted_kbi_quotes_obey_intrinsic_and_hard_price_bounds() { + let strike = 100 * SCALE; + for spot_dollars in [50u128, 75, 100, 125, 200] { + for rate in [0, 60_000_000_000, 120_000_000_000] { + for dividend_yield in [0, 60_000_000_000, 120_000_000_000] { + for sigma in [100_000_000_000, 300_000_000_000, 1_200_000_000_000] { + for maturity in [30 * SCALE / 365, SCALE / 2, 2 * SCALE] { + let spot = spot_dollars * SCALE; + let call = american_kbi_price( + spot, + strike, + rate, + dividend_yield, + sigma, + maturity, + AmericanKbiKind::Call, + ) + .expect("grid is inside the documented KBI domain"); + let put = american_kbi_price( + spot, + strike, + rate, + dividend_yield, + sigma, + maturity, + AmericanKbiKind::Put, + ) + .expect("grid is inside the documented KBI domain"); + + assert!(call >= spot.saturating_sub(strike)); + assert!(call <= spot); + assert!(put >= strike.saturating_sub(spot)); + assert!(put <= strike); + } + } + } + } + } + } + + #[test] + fn full_width_kbi_inputs_fail_closed_without_panicking() { + let iters: u64 = std::env::var("SOLMATH_FUZZ_ITERS") + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(40_000); + let mut rng = Rng(0x6b62_695f_6675_7a7a); + + for index in 0..iters { + let spot = if index % 8 == 0 { 0 } else { rng.next_u128() }; + let strike = if index % 8 == 1 { 0 } else { rng.next_u128() }; + let rate = rng.next_u128(); + let dividend_yield = rng.next_u128(); + let sigma = rng.next_u128(); + let maturity = rng.next_u128(); + let kind = if index & 1 == 0 { + AmericanKbiKind::Call + } else { + AmericanKbiKind::Put + }; + let _ = american_kbi_price(spot, strike, rate, dividend_yield, sigma, maturity, kind); + } + + let extrema = [0, 1, SCALE, u128::MAX]; + for &value in &extrema { + for kind in [AmericanKbiKind::Call, AmericanKbiKind::Put] { + let _ = american_kbi_price(value, value, value, value, value, value, kind); + let _ = + american_kbi_price(u128::MAX, 100 * SCALE, value, value, value, value, kind); + } + } + } +} + +#[cfg(feature = "sabr")] +mod sabr_invariants { + use super::*; + use solmath::{ + exp_fixed_hp, fp_mul_hp_i, + sabr::{ + certify_sabr_surface, CertifiedSabrSurface, MAX_SABR_SURFACE_MATURITIES, + MAX_SABR_SURFACE_QUOTES, MAX_SABR_SURFACE_STRIKES, + }, + sabr_price, + }; + + const HP_FACTOR: i128 = 1_000; + const CONVEXITY_TOLERANCE: u128 = 1_000; + + fn hp_discounted_strike(k: u128, r: u128, t: u128) -> u128 { + let rt = fp_mul_hp_i(r as i128 * HP_FACTOR, t as i128 * HP_FACTOR).unwrap(); + let discount = exp_fixed_hp(-rt).unwrap(); + let kd = fp_mul_hp_i(k as i128 * HP_FACTOR, discount).unwrap(); + ((kd + HP_FACTOR / 2) / HP_FACTOR) as u128 + } + + fn assert_certificate_is_exactly_borrowed( + certificate: CertifiedSabrSurface<'_>, + strikes: &[u128], + maturities: &[u128], + calls: &[u128], + puts: &[u128], + ) { + assert_eq!(certificate.spot(), 100 * SCALE); + assert_eq!(certificate.rate(), 0); + assert_eq!(certificate.strikes(), strikes); + assert_eq!(certificate.maturities(), maturities); + assert_eq!(certificate.quote_count(), calls.len()); + for maturity_index in 0..maturities.len() { + for strike_index in 0..strikes.len() { + let index = maturity_index * strikes.len() + strike_index; + let quote = certificate.quote_at(maturity_index, strike_index).unwrap(); + assert_eq!(quote.spot(), 100 * SCALE); + assert_eq!(quote.rate(), 0); + assert_eq!(quote.strike(), strikes[strike_index]); + assert_eq!(quote.maturity(), maturities[maturity_index]); + assert_eq!(quote.call(), calls[index]); + assert_eq!(quote.put(), puts[index]); + } + } + assert_eq!( + certificate.quote_at(maturities.len(), 0), + Err(SolMathError::DomainError) + ); + assert_eq!( + certificate.quote_at(0, strikes.len()), + Err(SolMathError::DomainError) + ); + } + + #[test] + fn public_surface_certificate_enforces_global_static_arbitrage() { + let strikes = [80 * SCALE, 100 * SCALE, 120 * SCALE]; + let maturities = [SCALE, 2 * SCALE, 3 * SCALE]; + let calls = [ + 25 * SCALE, + 12 * SCALE, + 5 * SCALE, + 27 * SCALE, + 15 * SCALE, + 8 * SCALE, + 30 * SCALE, + 18 * SCALE, + 10 * SCALE, + ]; + let puts = [ + 5 * SCALE, + 12 * SCALE, + 25 * SCALE, + 7 * SCALE, + 15 * SCALE, + 28 * SCALE, + 10 * SCALE, + 18 * SCALE, + 30 * SCALE, + ]; + + let certificate = + certify_sabr_surface(100 * SCALE, 0, &strikes, &maturities, &calls, &puts).unwrap(); + assert_certificate_is_exactly_borrowed(certificate, &strikes, &maturities, &calls, &puts); + + // One raw unit of parity residual must fail closed. + let mut parity_calls = calls; + parity_calls[4] += 1; + assert_eq!( + certify_sabr_surface(100 * SCALE, 0, &strikes, &maturities, &parity_calls, &puts,), + Err(SolMathError::NoConvergence) + ); + + // Preserve node-level parity and bounds but introduce a call calendar + // inversion. Only whole-surface validation can detect this. + let mut calendar_calls = calls; + let mut calendar_puts = puts; + calendar_calls[3] = 24 * SCALE; + calendar_puts[3] = 4 * SCALE; + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + 0, + &strikes, + &maturities, + &calendar_calls, + &calendar_puts, + ), + Err(SolMathError::NoConvergence) + ); + + // Every distant-wing node below has exact parity and hard bounds, but + // the irregular-strike butterfly is negative. + let wing_strikes = [50 * SCALE, 100 * SCALE, 200 * SCALE]; + let wing_maturities = [SCALE, 2 * SCALE]; + let wing_calls = [55 * SCALE, 54 * SCALE, SCALE, 55 * SCALE, 54 * SCALE, SCALE]; + let wing_puts = [ + 5 * SCALE, + 54 * SCALE, + 101 * SCALE, + 5 * SCALE, + 54 * SCALE, + 101 * SCALE, + ]; + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + 0, + &wing_strikes, + &wing_maturities, + &wing_calls, + &wing_puts, + ), + Err(SolMathError::NoConvergence) + ); + + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + 0, + &strikes, + &maturities, + &calls[..calls.len() - 1], + &puts, + ), + Err(SolMathError::DomainError) + ); + + let oversized_strikes = [SCALE; MAX_SABR_SURFACE_STRIKES + 1]; + let oversized_strike_calls = [0; (MAX_SABR_SURFACE_STRIKES + 1) * 2]; + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + 0, + &oversized_strikes, + &maturities[..2], + &oversized_strike_calls, + &oversized_strike_calls, + ), + Err(SolMathError::DomainError) + ); + let oversized_maturities = [SCALE; MAX_SABR_SURFACE_MATURITIES + 1]; + let oversized_maturity_calls = [0; 3 * (MAX_SABR_SURFACE_MATURITIES + 1)]; + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + 0, + &strikes, + &oversized_maturities, + &oversized_maturity_calls, + &oversized_maturity_calls, + ), + Err(SolMathError::DomainError) + ); + let quote_limit_strikes = [SCALE; MAX_SABR_SURFACE_QUOTES / 16 + 1]; + let quote_limit_maturities = [SCALE; 16]; + let quote_limit_calls = [0; (MAX_SABR_SURFACE_QUOTES / 16 + 1) * 16]; + assert_eq!( + certify_sabr_surface( + 100 * SCALE, + 0, + "e_limit_strikes, + "e_limit_maturities, + "e_limit_calls, + "e_limit_calls, + ), + Err(SolMathError::DomainError) + ); + } + + #[test] + fn accepted_sabr_surfaces_are_bounded_monotone_convex_and_in_parity() { + let surfaces = [ + (SCALE / 5, SCALE, 0, 0, SCALE, 0), + ( + SCALE / 5, + SCALE / 2, + -3 * SCALE_I / 10, + 2 * SCALE / 5, + SCALE, + 0, + ), + ( + SCALE / 10, + SCALE, + -SCALE_I / 5, + SCALE / 5, + SCALE / 2, + SCALE / 20, + ), + ]; + let strikes = [ + 60 * SCALE, + 70 * SCALE, + 80 * SCALE, + 90 * SCALE, + 100 * SCALE, + 110 * SCALE, + 120 * SCALE, + 130 * SCALE, + 140 * SCALE, + ]; + let s = 100 * SCALE; + + for &(alpha, beta, rho, nu, t, r) in &surfaces { + let mut calls = [0u128; 9]; + let mut puts = [0u128; 9]; + for (index, &k) in strikes.iter().enumerate() { + let (call, put) = + sabr_price(s, k, r, t, alpha, beta, rho, nu).unwrap_or_else(|error| { + panic!( + "certified SABR surface point failed: alpha={alpha}, beta={beta}, \ + rho={rho}, nu={nu}, t={t}, r={r}, k={k}: {error:?}" + ) + }); + let kd = hp_discounted_strike(k, r, t); + assert!(call <= s); + assert!(put <= k); + assert!(call >= s.saturating_sub(kd)); + assert!(put >= kd.saturating_sub(s)); + assert_eq!(call + kd, put + s); + calls[index] = call; + puts[index] = put; + } + + for index in 1..strikes.len() { + assert!( + calls[index - 1] >= calls[index], + "SABR calls increase with strike" + ); + assert!( + puts[index - 1] <= puts[index], + "SABR puts decrease with strike" + ); + } + for index in 1..strikes.len() - 1 { + assert!( + calls[index - 1] + .checked_add(calls[index + 1]) + .and_then(|value| value.checked_add(CONVEXITY_TOLERANCE)) + .unwrap() + >= calls[index].checked_mul(2).unwrap(), + "SABR call surface is not convex at strike {}", + strikes[index] + ); + assert!( + puts[index - 1] + .checked_add(puts[index + 1]) + .and_then(|value| value.checked_add(CONVEXITY_TOLERANCE)) + .unwrap() + >= puts[index].checked_mul(2).unwrap(), + "SABR put surface is not convex at strike {}", + strikes[index] + ); + } + } + } + + #[test] + fn sabr_execution_rejects_the_uncertified_asymptotic_regime() { + assert_eq!( + sabr_price( + 100 * SCALE, + 100 * SCALE, + 0, + SCALE, + SCALE / 5, + SCALE, + 0, + SCALE, + ), + Err(SolMathError::NoConvergence) + ); + } + + #[test] + fn extreme_sabr_inputs_fail_closed_without_panicking() { + let maximum = i128::MAX as u128; + let cases = [ + (u128::MAX, SCALE, 0, SCALE, SCALE / 5, SCALE, 0, 0), + (SCALE, u128::MAX, 0, SCALE, SCALE / 5, SCALE, 0, 0), + (SCALE, SCALE, u128::MAX, SCALE, SCALE / 5, SCALE, 0, 0), + (SCALE, SCALE, 0, u128::MAX, SCALE / 5, SCALE, 0, 0), + (SCALE, SCALE, 0, SCALE, u128::MAX, SCALE, 0, 0), + (SCALE, SCALE, 0, SCALE, SCALE / 5, u128::MAX, 0, 0), + (SCALE, SCALE, 0, SCALE, SCALE / 5, SCALE, i128::MIN, 0), + (SCALE, SCALE, 0, SCALE, SCALE / 5, SCALE, i128::MAX, 0), + (SCALE, SCALE, 0, SCALE, SCALE / 5, SCALE, 0, u128::MAX), + (maximum, SCALE, 0, SCALE, SCALE / 5, SCALE, 0, 0), + (SCALE, maximum, 0, SCALE, SCALE / 5, SCALE, 0, 0), + ]; + + for &(s, k, r, t, alpha, beta, rho, nu) in &cases { + let outcome = std::panic::catch_unwind(|| sabr_price(s, k, r, t, alpha, beta, rho, nu)); + assert!( + outcome.is_ok(), + "SABR panicked for s={s}, k={k}, r={r}, t={t}, alpha={alpha}, \ + beta={beta}, rho={rho}, nu={nu}" + ); + if let Ok((call, put)) = outcome.unwrap() { + assert!(call <= s); + assert!(put <= k); + } + } + } +} diff --git a/tests/rainbow_reference.rs b/tests/rainbow_reference.rs new file mode 100644 index 0000000..9578cdd --- /dev/null +++ b/tests/rainbow_reference.rs @@ -0,0 +1,98 @@ +//! Reference and invariant checks for two-asset rainbow options. +//! +//! Prices are the exact Stulz (1982) closed form; the hardcoded targets were +//! computed independently in Python and cross-checked against 6M-path Monte +//! Carlo to ~1e-3. Fixed-point reproduces them to <=1 ULP. +#![cfg(feature = "rainbow")] + +use solmath::{best_of_call, worst_of_call, SCALE}; + +fn fp(x: f64) -> u128 { + (x * SCALE as f64).round() as u128 +} +fn fpi(x: f64) -> i128 { + (x * SCALE as f64).round() as i128 +} + +#[test] +fn matches_stulz_reference() { + // (S1,S2,K,r,q1,q2,v1,v2,rho,T, worst_ref, best_ref) + let cases = [ + ( + 100., 100., 100., 0.05, 0.0, 0.0, 0.2, 0.25, 0.5, 1.0, 5.6776, 17.1090, + ), + ( + 100., 110., 100., 0.03, 0.02, 0.01, 0.3, 0.2, 0.3, 0.5, 4.6413, 16.8314, + ), + ( + 90., 100., 95., 0.05, 0.0, 0.0, 0.4, 0.35, 0.7, 2.0, 13.3043, 34.3904, + ), + ( + 120., 80., 100., 0.04, 0.0, 0.0, 0.25, 0.45, -0.3, 1.0, 2.5400, 33.0052, + ), + ]; + for (s1, s2, k, r, q1, q2, v1, v2, rho, t, w, b) in cases { + let mn = worst_of_call( + fp(s1), + fp(s2), + fp(k), + fp(r), + fp(q1), + fp(q2), + fp(v1), + fp(v2), + fpi(rho), + fp(t), + ) + .unwrap() as f64 + / SCALE as f64; + let mx = best_of_call( + fp(s1), + fp(s2), + fp(k), + fp(r), + fp(q1), + fp(q2), + fp(v1), + fp(v2), + fpi(rho), + fp(t), + ) + .unwrap() as f64 + / SCALE as f64; + assert!((mn - w).abs() < 5e-4, "worst_of {mn} vs {w}"); + assert!((mx - b).abs() < 5e-4, "best_of {mx} vs {b}"); + } +} + +#[test] +fn worst_of_never_exceeds_best_of() { + // min(S1,S2) <= max(S1,S2) pointwise, so the worst-of call <= best-of call. + let mut rng = 0x1234_5678_u64; + let mut next = || { + rng ^= rng << 13; + rng ^= rng >> 7; + rng ^= rng << 17; + rng + }; + for _ in 0..5000 { + let s1 = fp(50.0 + (next() % 100) as f64); + let s2 = fp(50.0 + (next() % 100) as f64); + let k = fp(50.0 + (next() % 100) as f64); + let r = fp((next() % 10) as f64 / 100.0); + let v1 = fp(0.1 + (next() % 60) as f64 / 100.0); + let v2 = fp(0.1 + (next() % 60) as f64 / 100.0); + let rho = fpi(-0.9 + (next() % 180) as f64 / 100.0); + let t = fp(0.1 + (next() % 30) as f64 / 10.0); + let out = std::panic::catch_unwind(|| { + ( + worst_of_call(s1, s2, k, r, 0, 0, v1, v2, rho, t), + best_of_call(s1, s2, k, r, 0, 0, v1, v2, rho, t), + ) + }); + assert!(out.is_ok(), "rainbow panicked"); + if let Ok((Ok(w), Ok(b))) = out { + assert!(w <= b + SCALE / 1000, "worst {w} > best {b}"); + } + } +} diff --git a/tests/sabr_quantlib_reference.rs b/tests/sabr_quantlib_reference.rs new file mode 100644 index 0000000..53291a3 --- /dev/null +++ b/tests/sabr_quantlib_reference.rs @@ -0,0 +1,7 @@ +#![cfg(feature = "sabr")] + +// Keep the large generated QuantLib corpus outside the library unit-test +// build. This preserves repository coverage while allowing `cargo test` on +// the published crate, whose compact allowlist deliberately excludes +// repository-only generated corpora. +include!("../test_data/sabr_reference_tests.rs");