# Conventions Reference (/docs/conventions) Lookup table for all string parameter values accepted by vade. *** Frequencies [#frequencies] | Value | Description | Used by | | ----- | ----------------------------- | ----------------------------------- | | `"M"` | Monthly (1-month periods) | `Schedule`, all instruments | | `"Q"` | Quarterly (3-month periods) | `Schedule`, all instruments | | `"S"` | Semi-annual (6-month periods) | `Schedule`, all instruments | | `"A"` | Annual (12-month periods) | `Schedule`, all instruments | | `"Z"` | Zero-coupon (single period) | `Schedule`, zero-coupon instruments | Day Count Conventions [#day-count-conventions] | Value | Description | Used by | | --------------- | ---------------------------------------- | --------------------------------------------- | | `"act360"` | Actual/360 | `dcf()`, most IR instruments, `DiscountCurve` | | `"act365f"` | Actual/365 Fixed | `dcf()`, GBP instruments, `ForwardCurve` | | `"act365.25"` | Actual/365.25 | `dcf()` | | `"act364"` | Actual/364 | `dcf()` | | `"actactisda"` | Actual/Actual ISDA | `dcf()`, bond accrual | | `"30/360"` | 30/360 (US) | `dcf()`, USD bonds | | `"30e/360"` | 30E/360 (European) | `dcf()`, EUR bonds | | `"30e/360isda"` | 30E/360 ISDA (requires termination date) | `dcf()` | | `"bus252"` | Business/252 (requires calendar) | `dcf()`, BRL instruments | | `"one"` | Always returns 1.0 | `dcf()` | Aliases: `"actual360"` for `"act360"`, `"act365fixed"` for `"act365f"`, `"actact"` / `"actualactual"` for `"actactisda"`, `"30360"` / `"thirty360"` for `"30/360"`, `"30e360"` for `"30e/360"`, `"30e360isda"` for `"30e/360isda"`, `"bus/252"` for `"bus252"`, `"1"` / `"1/1"` for `"one"`. Business Day Adjusters [#business-day-adjusters] | Value | Description | Used by | | ---------------------- | ----------------------------------------------------- | ------------------------------------- | | `"modified_following"` | Roll forward; if month changes, roll backward instead | `Schedule` (default), all instruments | | `"following"` | Roll forward to next business day | `Schedule`, instruments | | `"preceding"` | Roll backward to previous business day | `Schedule`, instruments | | `"modified_preceding"` | Roll backward; if month changes, roll forward instead | `Schedule`, instruments | | `"actual"` | No adjustment | `Schedule` | Aliases: `"mf"` / `"mod_following"` / `"modfollowing"` for `"modified_following"`, `"fol"` for `"following"`, `"pre"` for `"preceding"`, `"mp"` / `"mod_preceding"` / `"modpreceding"` for `"modified_preceding"`, `"none"` for `"actual"`. Roll Day [#roll-day] | Value | Description | Used by | | ---------- | ----------------------------------------------- | ---------- | | `"eom"` | End of month | `Schedule` | | `"imm"` | IMM date (3rd Wednesday) | `Schedule` | | `1` - `31` | Specific day of month (clamped to month length) | `Schedule` | Stub Inference [#stub-inference] | Value | Description | Used by | | --------------- | --------------------------------- | ---------- | | `"short_front"` | Short stub at the front (default) | `Schedule` | | `"long_front"` | Long stub at the front | `Schedule` | | `"short_back"` | Short stub at the back | `Schedule` | | `"long_back"` | Long stub at the back | `Schedule` | Interpolation Methods [#interpolation-methods] | Value | Description | Used by | | -------------------- | ---------------------------------------------- | ---------------------------- | | `"log_linear"` | Log-linear (piecewise constant forward rates) | `DiscountCurve` (default) | | `"linear"` | Linear interpolation | `LineCurve` (default) | | `"linear_zero_rate"` | Linear in zero-rate space | `DiscountCurve`, `LineCurve` | | `"flat_forward"` | Flat forward rates (step function) | `ForwardCurve` (default) | | `"flat_backward"` | Flat backward rates | `ForwardCurve` | | `"natural_cubic"` | Natural cubic spline (C2-continuous) | `DiscountCurve`, `LineCurve` | | `"log_cubic"` | Log-cubic spline (positive DFs guaranteed) | `DiscountCurve` | | `"hermite"` | Hermite cubic with finite-difference tangents | `DiscountCurve`, `LineCurve` | | `"monotone_cubic"` | Fritsch-Carlson monotonicity-preserving | `DiscountCurve`, `LineCurve` | | `"convex_monotone"` | Hagan-West convex monotone (positive forwards) | `DiscountCurve` | Named Calendars [#named-calendars] | Value | Description | | ------- | ------------------ | | `"NYC"` | New York | | `"LDN"` | London | | `"TGT"` | TARGET (Eurozone) | | `"TYO"` | Tokyo | | `"SYD"` | Sydney | | `"ZUR"` | Zurich | | `"STK"` | Stockholm | | `"OSL"` | Oslo | | `"TRO"` | Toronto | | `"FED"` | US Federal Reserve | | `"WLG"` | Wellington | | `"MUM"` | Mumbai | | `"MEX"` | Mexico City | | `"BJS"` | Beijing | | `"NSW"` | New South Wales | Tenor Strings [#tenor-strings] | Format | Example | Description | | ------ | ------- | ----------------------------------- | | `"NY"` | `"2Y"` | N years | | `"NM"` | `"6M"` | N months | | `"NW"` | `"1W"` | N weeks | | `"ND"` | `"3D"` | N calendar days | | `"NB"` | `"3B"` | N business days (requires calendar) | # Vade (/docs) **Production-grade Fixed Income Quant Library with rates, credit and FX analytics for Python, powered by Rust.** Vade brings institutional-quality fixed income analytics to Python. Built from the ground up in Rust and exposed through seamless PyO3 bindings, it delivers the performance of a compiled trading system with the ergonomics of a Python library. *** Why Vade? [#why-vade] * **Rust-powered performance** -- Native speed for curve building, calibration, and risk computation. No compromises. * **Automatic differentiation** -- First- and second-order sensitivities (delta, gamma) computed exactly via dual numbers -- no finite differences, no numerical noise. * **Full fixed income coverage** -- Interest rate swaps, bonds, CDS, FX forwards, caps & floors, callable bonds, and more. * **Multi-curve calibration** -- Simultaneous bootstrap and fitting across discount, forecast, and credit curves. * **Market context management** -- Bundle curves, FX rates, and fixings into a single serializable environment. * **Zero Python overhead** -- All analytics execute in Rust. Python is the interface, not the bottleneck. *** Getting Started [#getting-started] Get vade installed and running in under 5 minutes. * [Installation](getting-started/installation) -- Install vade and verify your setup Understanding Vade [#understanding-vade] Learn how vade works under the hood -- the Rust/Python architecture and the automatic differentiation type system that powers risk analytics. * [Architecture](getting-started/architecture) -- Rust core, PyO3 bindings, max-Rust philosophy, and performance * [Type System](getting-started/type-system) -- Dual/Dual2 automatic differentiation and Union return types API Reference [#api-reference] Complete parameter signatures, types, and working examples for every vade class and function. * [API Reference](api/) -- Full API index with product sections and shared infrastructure By Product [#by-product] * [Rates](api/rates/) -- Interest rate curves, instruments, and calibration * [FX](api/fx/) -- FX rates, forwards, cross-currency, and non-deliverable instruments * [Credit](api/credit/) -- Bonds, CDS, callable bonds, and spread analytics Shared Infrastructure [#shared-infrastructure] * [Calendar](api/calendar) -- Schedule generation, business day calendars, day count fractions, tenor arithmetic * [Autodiff](api/autodiff) -- Dual and Dual2 automatic differentiation types * [Numerical](api/numerical) -- Root-finding solvers (Brent, Newton-Raphson) * [Context](api/context) -- MarketContext, FixingStore, and evaluation date management * [Cashflows](api/cashflows) -- Fixed and floating leg cashflow generation Fundamentals [#fundamentals] Core concepts that apply across all product areas. * [Quick Start](guides/quick-start) -- Build a curve, price an instrument, and calibrate in one workflow * [Market Context](guides/market-context) -- Build, query, and serialize a complete market environment * [Serialization](guides/serialization) -- JSON round-trips for curves and instruments * [Calibration](guides/calibration) -- Multi-curve calibration with Solver Rates [#rates] Interest rate curve construction, instrument pricing, risk analytics, and volatility products. * [Overview](guides/rates/) -- Product introduction and key concepts * [Curve Building](guides/rates/curve-building) -- Interpolation methods, curve types, and forward rate analysis * [Bootstrap & Parametric Curves](guides/rates/bootstrap-parametric) -- Iterative bootstrap, Nelson-Siegel, NSS, Smith-Wilson * [Pricing](guides/rates/pricing) -- Price IR instruments against calibrated curves * [Risk](guides/rates/risk) -- Delta, gamma, and bucket-level sensitivities * [Caps & Floors](guides/rates/capfloor) -- Black-76 and Bachelier pricing Credit [#credit] Bond analytics, credit default swaps, credit curve construction, and spread analysis. * [Overview](guides/credit/) -- Product introduction and key concepts * [Bonds](guides/credit/bonds) -- Bond analytics, duration, convexity, Z-spread * [Callable Bonds](guides/credit/callable-bonds) -- Callable bond pricing with OAS * [Credit Curves & CDS](guides/credit/credit-curves-cds) -- Credit curve construction and CDS pricing * [Fitted Curves](guides/credit/fitted-curves) -- Parametric curve fitting to bond portfolios * [Spread Analytics](guides/credit/spread-analytics) -- I-spread and asset swap analysis FX [#fx] FX spot rate triangulation, forward construction, cross-currency swaps, and non-deliverable instruments. * [Overview](guides/fx/) -- Product introduction and key concepts * [FX Rates & Forwards](guides/fx/fx-rates-forwards) -- FX rate triangulation and forward construction * [Cross-Currency Swaps](guides/fx/cross-currency) -- Cross-currency swap pricing and risk * [Non-Deliverable Instruments](guides/fx/non-deliverable) -- NDF and non-deliverable swap pricing Learning Paths [#learning-paths] **New to Vade?** Start here: [Installation](getting-started/installation) -> [Architecture](getting-started/architecture) -> [Type System](getting-started/type-system) -> [Quick Start](guides/quick-start) **Rates:** [Curve Building](guides/rates/curve-building) -> [Bootstrap & Parametric](guides/rates/bootstrap-parametric) -> [Calibration](guides/calibration) -> [Pricing](guides/rates/pricing) -> [Risk](guides/rates/risk) -> [Caps & Floors](guides/rates/capfloor) **Credit:** [Bonds](guides/credit/bonds) -> [Credit Curves & CDS](guides/credit/credit-curves-cds) -> [Spread Analytics](guides/credit/spread-analytics) -> [Callable Bonds](guides/credit/callable-bonds) -> [Fitted Curves](guides/credit/fitted-curves) **FX:** [FX Rates & Forwards](guides/fx/fx-rates-forwards) -> [Cross-Currency Swaps](guides/fx/cross-currency) -> [Non-Deliverable Instruments](guides/fx/non-deliverable) **Market data management:** [Market Context](guides/market-context) -> [Serialization](guides/serialization) Roadmap [#roadmap] See [Roadmap](./roadmap) for planned features and milestones. Reference [#reference] Lookup tables for string enum values and parameter conventions used across the API. * [Conventions](./conventions) -- String enum values for day counts, frequencies, interpolation methods, and more # Vade — Product Roadmap (/docs/roadmap) Features and development pipeline, organized by asset class and infrastructure layer. For technical documentation on shipped features, see the [main index](index). **Status:** Available = production-ready | Planned = scoped for near-term delivery | Future = strategic roadmap *** Rates [#rates] | Status | Feature | Description | Key Capabilities | | --------- | --------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Available | **Interest Rate Swaps** | Vanilla and structured IRS: fixed-float, basis, zero-coupon, OIS | IRS, FRA, ZCS, SBS, OIS with spec-driven construction, NPV, par rate, cashflow projection, spread analytics, delta/gamma via AD | | Available | **Deposits & Futures** | Money market deposits and short-rate futures | Deposit rate/NPV, IR futures with convexity adjustment, pack/bundle aggregation | | Available | **Caps & Floors** | Caplet/floorlet pricing under Black-76 and Bachelier (normal) models | NPV, implied vol inversion, vol surface construction, normal and lognormal pricing | | Available | **Discount Curves** | Discount factor term structures with 10+ interpolation schemes | Log-linear, linear, flat forward/backward, monotone cubic, log-cubic, natural cubic, Hermite, convex monotone, cubic spline, linear zero; LineCurve for generic value curves | | Available | **Forward Curves** | Native forward-rate representation with Gauss-Legendre quadrature | Forward rate node specification, automatic discount factor recovery via numerical integration | | Available | **Parametric Curves** | Nelson-Siegel, Svensson, and Smith-Wilson term structure models | Analytical zero and instantaneous forward rates, closed-form calibration targets | | Available | **Composite & Spread Curves** | Multi-curve layering and additive/multiplicative spread overlays | CompositeCurve (stacked), SpreadCurve (base + deterministic spread), TurnOfYearCurve (date-specific jump adjustments) | | Available | **Curve Transforms** | Non-destructive curve manipulation operators | Parallel shift (bp), time translation, roll-down (theta decay) — applicable across all curve types | | Available | **Multi-Curve & CSA Discounting** | Simultaneous OIS discounting and forecast curve construction | Iterative node-by-node bootstrap, multi-curve Jacobian calibration, pre-solver dependency chains | | Available | **Solver & Calibration** | Jacobian-based multi-instrument curve fitting | Levenberg-Marquardt, Gauss-Newton, gradient descent; convergence tolerances, condition number monitoring | | Available | **Risk Analytics** | Bucket-level sensitivities via automatic differentiation | IRImpliedCurve with user-defined tenor buckets, bucket delta, full AD propagation through the pricing stack | | Available | **FX Implied Curves** | Cross-currency discount curves from FX forward markets | Interest rate parity-derived discount factors for cross-currency valuation | | Planned | **Swaptions** | European and Bermudan swaption pricing | Payer/receiver NPV, delta/gamma via AD, expiry × tenor vol surface interpolation, cash and physical settlement | | Future | **Structured Rate Notes** | Exotic coupon structures: range accruals, inverse floaters, snowballs | Path-dependent coupon evaluation, scenario analysis, rate curve and vol surface sensitivities | | Future | **Bond Futures** | Exchange-traded bond delivery contracts | Forward pricing with carry and financing, CTD identification, gross/net basis, delivery option analytics | Credit [#credit] | Status | Feature | Description | Key Capabilities | | --------- | ------------------------ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | Available | **Fixed Rate Bonds** | Settlement-aware bond pricing with full analytics | NPV, dirty/clean price, YTM, Macaulay/modified duration, convexity, Z-spread, I-spread | | Available | **Bills** | Discount instruments: T-bills, CP, CD | Discount yield, money market yield, settlement-adjusted pricing | | Available | **Floating Rate Notes** | FRN with sub-period compounding and embedded optionality | FloatRateNote, SubPeriodFRN, CappedFloatRateNote with Black-76/Bachelier cap/floor valuation | | Available | **Structured Bonds** | Non-vanilla fixed income: zero-coupon, step-up, amortizing, PIK | OID accrual (zeros), varying coupon schedules (step-up), declining notional (amortizing), in-kind compounding (PIK) | | Available | **Callable Bonds** | Callable bond valuation via Hull-White trinomial lattice | OAS, effective duration/convexity, vega, arbitrary call schedule support | | Available | **Asset Swaps** | Par and market-value asset swap spread analytics | Bond + swap decomposition, asset swap spread extraction | | Available | **Credit Default Swaps** | Single-name CDS pricing with hazard rate bootstrapping | CDS NPV, par spread, survival probabilities, CreditImpliedCurve construction | | Available | **Fitted Bond Curves** | Parametric curve fitting to observed bond prices via NLS | NS/NSS/Smith-Wilson models, Levenberg-Marquardt optimization, residual diagnostics | | Future | **Credit Linked Notes** | Funded credit exposure via note wrapper | CLN pricing (credit + funding + recovery), recovery sensitivity, break-even spread vs unfunded CDS | | Future | **Credit Index Options** | Options on CDX/iTraxx index spreads | Payer/receiver pricing, index spread vol, exercise into tranche, delta/gamma to index spread | | Future | **Fixed Income ETFs** | Bond ETF analytics and creation/redemption mechanics | NAV premium/discount, effective duration/convexity, YTW, tracking error, creation basket composition | | Future | **CLOs/ABS** | Structured credit with prepayment and default modeling | Pool-level cashflow projection, tranche yield/WAL/duration, CPR/CDR/severity scenario analysis | FX & FX Options [#fx--fx-options] | Status | Feature | Description | Key Capabilities | | --------- | ------------------------------------ | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | Available | **FX Rates & Forwards** | Spot rate graph with BFS triangulation and covered interest rate parity forwards | Cross-rate discovery, forward point calculation, outright forward rates | | Available | **Cross-Currency Swaps** | MTM and non-MTM cross-currency basis swaps | MTM resets, notional exchange, multi-curve CSA-aware pricing | | Available | **FX Forwards** | Outright FX forward contracts | NPV, forward points, settlement and value date handling | | Available | **Non-Deliverable Instruments** | NDF, NDIRS, NDXCS for restricted EM currencies | Per-period FX conversion, 3-curve pricing framework, 5 EM calendars, 15 market convention specs | | Available | **FX Implied Curves** | Discount curves bootstrapped from FX forward points | Cross-currency discounting via interest rate parity | | Planned | **Full EMTA NDF Templates** | Complete EMTA-standard NDF templates across EM currency pairs | Construction from CCY pair alone, fixing source registry, settlement calendar integration | | Future | **Vanilla FX Options & Vol Surface** | European FX option pricing and smile construction | Greeks, delta/premium-adjusted conventions, RR/BF quotes, ATM definitions, smile interpolation, vol cones | | Future | **Vanilla Option Strategies** | Multi-leg vanilla structures | Spreads, risk reversals, straddles, strangles, butterflies, seagulls | | Future | **First-Generation Exotics** | Single-asset path-dependent options | Barriers (KI/KO), digitals, touches, compounds, Asians, lookbacks, forward starts, cliquets, quantos | | Future | **Second-Generation Exotics** | Advanced exotics and multi-asset structures | Corridors, faders, exotic barriers, pay-later, spread/exchange, basket, best/worst-of, var/vol swaps | | Future | **Structured FX Forwards** | Forwards with embedded optionality | Participating, shark, accumulator, range accrual, knock-out, butterfly, boomerang, forward series | | Future | **FX Structured Products** | Deposits, loans, swaps, and notes with FX-linked payoffs | Dual currency deposits, performance-linked structures, participation notes, hybrid FX products | Infrastructure, Portfolio & Risk [#infrastructure-portfolio--risk] | Status | Feature | Description | Key Capabilities | | --------- | -------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Available | **Automatic Differentiation** | Forward-mode AD via Dual and Dual2 number types | First-order (delta) and second-order (gamma) sensitivities; exact analytic gradients through the full pricing stack | | Available | **Calendars & Scheduling** | Business day calendars, schedule generation, day count conventions | 15+ named calendars (NYC, LDN, TGT, TYO, etc.), 10 DCF conventions, stub handling, IMM date generation | | Available | **Cashflow Engine** | Fixed and floating leg generation with period-level granularity | FixedLeg, FloatLeg, structured cashflow output via Polars DataFrames | | Available | **Market Context** | Centralized market state container | Curves, FX rates, fixings; immutable functional API (.with\_\*()), solver integration, full JSON round-trip | | Available | **JSON Serialization** | Complete round-trip serialization across the object model | Polymorphic curve envelope (11 types), instrument serialization, MarketContext persistence | | Available | **Numerical Methods** | Root-finding and optimization primitives | Brent, Newton-Raphson; Black-76 and Bachelier closed-form models | | Planned | **MCP Server** | Model Context Protocol server exposing Vade's pricing and analytics as tool endpoints for LLM agents | Curve construction, instrument pricing, risk analytics, and market data operations accessible via MCP; structured JSON request/response; composable tool definitions for multi-step workflows | | Planned | **Agentic Workflow Integration** | LLM-orchestrated multi-step quantitative workflows via tool-use chains | Natural language-driven curve building, trade pricing, scenario analysis, and risk reporting; agent-composed pipelines across rates, credit, and FX; iterative calibration and what-if analysis through conversational interfaces | | Planned | **Portfolio Valuation Engine** | Trade store, book hierarchy, and position management | Trade capture with audit trail, book/portfolio NPV aggregation, position lifecycle, bulk repricing | | Planned | **Risk & PnL Engine** | Portfolio-level risk measures and PnL computation | Parametric and historical VaR, user-defined stress scenarios, daily/intraday PnL, risk limit monitoring | | Planned | **PnL Explain** | Attribution of PnL into fundamental market and trade drivers | Carry/roll-down, curve decomposition (parallel/slope/curvature), trade activity effects, unexplained residual | Financing & Rates-Credit Hybrids [#financing--rates-credit-hybrids] | Status | Feature | Description | Key Capabilities | | ------ | ---------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | | Future | **Repo** | Repurchase agreement pricing and collateral management | NPV, implied repo rate, margin call triggers, term and open repo support | | Future | **Reverse Repo** | Cash-lending perspective, mirroring repo mechanics | Reverse repo yield, collateral eligibility, net position analysis | | Future | **Securities Lending** | Fee accrual, collateral management, utilization tracking | Lending fee schedules, utilization rates, collateral coverage monitoring | | Future | **Syndicated Loans** | Multi-lender term loans and revolvers | Cashflow projection, all-in yield with upfront/commitment/utilization fees, prepayment with make-whole | | Future | **Bilateral Loans** | Single-lender facilities with flexible amortization profiles | NPV/YTM, duration/convexity, schedule generation (equal principal, annuity, bullet) | | Future | **Total Return Swaps** | Synthetic total return exposure vs floating funding rate | TRS NPV (receiver/payer legs), return projection, funding accrual, basis risk decomposition | | Future | **Bond Forwards** | Derivative based financing with funding curve adjustments | Forward clean price with carry/financing risk | XVA & Valuation Adjustments [#xva--valuation-adjustments] | Status | Feature | Description | Key Capabilities | | ------ | --------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | Future | **Exposure Simulation Engine** | Monte Carlo portfolio revaluation for counterparty exposure profiling | EPE/ENE time profiles, netting set aggregation, path-dependent collateral simulation, multi-factor scenario generation | | Future | **CSA & Collateral Modeling** | Credit Support Annex representation and collateral mechanics | Threshold/MTA/rounding, eligible collateral and haircut schedules, collateral interest accrual, rehypothecation | | Future | **CVA/DVA** | Bilateral counterparty credit valuation adjustments | EPE/ENE × survival probability × LGD framework, CDS-implied hazard rates, wrong-way risk | | Future | **FVA** | Funding valuation adjustment for uncollateralized exposures | Funding spread curves (repo, CP), secured vs unsecured exposure gap, bilateral FVA | | Future | **MVA** | Margin valuation adjustment for IM/VM funding cost | SIMM-based IM calculation, IM path evolution, margin funding cost attribution, clearing vs bilateral | | Future | **KVA** | Capital valuation adjustment for regulatory capital consumption | SA-CCR/IMM RWA, cost of equity hurdle rate, capital relief from collateral and central clearing | | Future | **ColVA** | Collateral valuation adjustment for haircut and reinvestment drag | Haircut opportunity cost, reinvestment gap, collateral optionality, currency and asset-type effects | | Future | **XVA Integration & Attribution** | Unified XVA framework with P\&L explain | Total XVA = CVA + DVA + FVA + MVA + KVA + ColVA, XVA Greeks (spread delta, funding delta), bid-offer adjustment, XVA P\&L attribution | # Autodiff (/docs/api/autodiff) Forward-mode automatic differentiation types for computing exact derivatives. `Dual` tracks first-order derivatives (gradient), `Dual2` tracks both first and second-order derivatives (gradient and Hessian). For a conceptual introduction to when and why these types appear, see the [type system guide](../getting-started/type-system). Related: [Risk Guide](../guides/rates/risk) demonstrates AD-based delta and gamma computation using Dual types. Dual [#dual] First-order dual number for forward-mode automatic differentiation. Constructor [#constructor] ```bash d = Dual(var, real) ``` | Name | Type | Default | Description | | ------ | ------- | -------- | ------------- | | `var` | `str` | required | Variable name | | `real` | `float` | required | Real value | Static Methods [#static-methods] new_multi_py [#new_multi_py] Create a dual number tracking multiple variables at once. ```bash d = Dual.new_multi_py(real, vars, dual=None) ``` | Name | Type | Default | Description | | ------ | ----------------------- | -------- | --------------------------------------------- | | `real` | `float` | required | Real value | | `vars` | `List[str]` | required | Variable names | | `dual` | `Optional[List[float]]` | `None` | Derivative seed values (defaults to identity) | Properties [#properties] | Property | Type | Description | | -------- | ----------- | ------------------------------- | | `.real` | `float` | The real (scalar) value | | `.vars` | `List[str]` | Variable names this dual tracks | Methods [#methods] | Method | Returns | Description | | ------------- | --------------- | --------------------------------------------------------------- | | `.gradient()` | `numpy.ndarray` | First-order partial derivatives as 1D array, ordered by `.vars` | | `.exp()` | `Dual` | Exponential with AD propagation | | `.log()` | `Dual` | Natural logarithm with AD propagation | Operators [#operators] | Operator | Left | Right | Result | | ----------- | ------ | --------------- | ------- | | `+` | `Dual` | `Dual \| float` | `Dual` | | `-` | `Dual` | `Dual \| float` | `Dual` | | `*` | `Dual` | `Dual \| float` | `Dual` | | `/` | `Dual` | `Dual \| float` | `Dual` | | `**` | `Dual` | `float` | `Dual` | | `-` (unary) | `Dual` | | `Dual` | | `float()` | `Dual` | | `float` | All binary operators also support `float` on the left side (`__radd__`, `__rsub__`, `__rmul__`, `__rtruediv__`). Examples [#examples] Basic gradient computation: ```python from vade import Dual # Create dual numbers for two variables x = Dual("x", 3.0) y = Dual("y", 4.0) # Compute f(x, y) = x^2 + x*y f = x ** 2.0 + x * y print(f.real) # 21.0 print(f.vars) # ['x', 'y'] print(f.gradient()) # [10. 3.] -- df/dx=2x+y=10, df/dy=x=3 ``` Multi-variable dual with explicit seed: ```python from vade import Dual d = Dual.new_multi_py(2.0, ["a", "b"], [1.0, 0.0]) result = d.exp() print(round(result.real, 6)) # 7.389056 print(round(float(result.gradient()[0]), 6)) # 7.389056 -- d(e^x)/dx = e^x ``` Dual2 [#dual2] Second-order dual number for forward-mode automatic differentiation. Tracks both first and second-order derivatives in a single forward pass. Constructor [#constructor-1] ```bash d = Dual2(var, real) ``` | Name | Type | Default | Description | | ------ | ------- | -------- | ------------- | | `var` | `str` | required | Variable name | | `real` | `float` | required | Real value | Static Methods [#static-methods-1] new_multi_py [#new_multi_py-1] Create a second-order dual number tracking multiple variables. ```bash d = Dual2.new_multi_py(real, vars, dual=None, dual2=None) ``` | Name | Type | Default | Description | | ------- | ----------------------- | -------- | ------------------------------------ | | `real` | `float` | required | Real value | | `vars` | `List[str]` | required | Variable names | | `dual` | `Optional[List[float]]` | `None` | First-order derivative seed values | | `dual2` | `Optional[List[float]]` | `None` | Half-Hessian seed values (flattened) | Properties [#properties-1] | Property | Type | Description | | -------- | ----------- | ------------------------------- | | `.real` | `float` | The real (scalar) value | | `.vars` | `List[str]` | Variable names this dual tracks | Methods [#methods-1] | Method | Returns | Description | | -------------- | --------------- | -------------------------------------------------------------------- | | `.gradient()` | `numpy.ndarray` | First-order partial derivatives as 1D array | | `.gradient2()` | `numpy.ndarray` | Full Hessian matrix as 2D array (returns 2x the stored half-Hessian) | | `.exp()` | `Dual2` | Exponential with AD propagation | | `.log()` | `Dual2` | Natural logarithm with AD propagation | Operators [#operators-1] | Operator | Left | Right | Result | | ----------- | ------- | ---------------- | ------- | | `+` | `Dual2` | `Dual2 \| float` | `Dual2` | | `-` | `Dual2` | `Dual2 \| float` | `Dual2` | | `*` | `Dual2` | `Dual2 \| float` | `Dual2` | | `/` | `Dual2` | `Dual2 \| float` | `Dual2` | | `**` | `Dual2` | `float` | `Dual2` | | `-` (unary) | `Dual2` | | `Dual2` | | `float()` | `Dual2` | | `float` | All binary operators also support `float` on the left side (`__radd__`, `__rsub__`, `__rmul__`, `__rtruediv__`). Example [#example] ```python from vade import Dual2 # Second-order derivatives: f(x) = x^3 x = Dual2("x", 2.0) f = x ** 3.0 print(f.real) # 8.0 print(f.gradient()) # [12.] -- df/dx = 3x^2 = 12 print(f.gradient2()) # [[12.]] -- d2f/dx2 = 6x = 12 ``` *** See [Numerical](numerical) for root-finding solvers that accept AD-computed derivatives via `newton_raphson_solve`. # Calendar (/docs/api/calendar) Date scheduling, business day calendars, day count fractions, and tenor arithmetic. Related: [Pricing Guide](../guides/rates/pricing) demonstrates Schedule-based cashflow generation with business day calendars. All types are available via flat import: ```bash from vade import Schedule, BusinessCalendar, dcf, add_tenor, add_business_days ``` See [Conventions](../conventions) for all accepted string parameter values. Schedule [#schedule] Financial schedule with regular periods and optional stubs. ```bash Schedule( effective, termination, frequency, calendar=None, adjuster="modified_following", roll_day=None, front_stub=None, back_stub=None, stub_inference="short_front", ) ``` Parameters [#parameters] | Name | Type | Default | Description | | ---------------- | ---------------------------- | ---------------------- | -------------------------------------------------------------------------------- | | `effective` | `datetime.date` | *required* | Start date of the schedule | | `termination` | `datetime.date` | *required* | End date of the schedule | | `frequency` | `str` | *required* | Period frequency (`"M"`, `"Q"`, `"S"`, `"A"`, `"Z"`) | | `calendar` | `BusinessCalendar` or `None` | `None` | Business day calendar (weekends-only if not provided) | | `adjuster` | `str` | `"modified_following"` | Business day adjustment rule | | `roll_day` | `str`, `int`, or `None` | `None` | Roll day convention (`"eom"`, `"imm"`, or `1`-`31`) | | `front_stub` | `datetime.date` or `None` | `None` | Explicit front stub date | | `back_stub` | `datetime.date` or `None` | `None` | Explicit back stub date | | `stub_inference` | `str` | `"short_front"` | Stub preference (`"short_front"`, `"long_front"`, `"short_back"`, `"long_back"`) | See [Conventions](../conventions) for all accepted values for `frequency`, `adjuster`, `roll_day`, and `stub_inference`. Properties [#properties] | Property | Type | Description | | ------------------- | --------------------- | -------------------------------- | | `.unadjusted_dates` | `list[datetime.date]` | Unadjusted period boundary dates | | `.adjusted_dates` | `list[datetime.date]` | Adjusted period boundary dates | | `.n_periods` | `int` | Number of periods | Methods [#methods] | Method | Returns | Description | | ------------------- | ------- | ------------------------------------- | | `.has_front_stub()` | `bool` | Whether the schedule has a front stub | | `.has_back_stub()` | `bool` | Whether the schedule has a back stub | Example [#example] ```python import datetime from vade import Schedule, BusinessCalendar schedule = Schedule( effective=datetime.date(2024, 1, 15), termination=datetime.date(2025, 1, 15), frequency="Q", calendar=BusinessCalendar("NYC"), ) schedule.n_periods # 4 schedule.adjusted_dates # [datetime.date(2024, 1, 16), datetime.date(2024, 4, 15), datetime.date(2024, 7, 15), datetime.date(2024, 10, 15), datetime.date(2025, 1, 15)] schedule.has_front_stub() # False schedule.has_back_stub() # False ``` Note: the effective date 2024-01-15 (Martin Luther King Jr. Day) is adjusted to 2024-01-16 in the adjusted dates. BusinessCalendar [#businesscalendar] Business day calendar with holidays and weekend conventions. ```bash BusinessCalendar(name) ``` Parameters [#parameters-1] | Name | Type | Default | Description | | ------ | ----- | ---------- | ----------------------------------------------------------- | | `name` | `str` | *required* | Named calendar identifier (e.g., `"NYC"`, `"LDN"`, `"TGT"`) | See [Conventions](../conventions#named-calendars) for all named calendars. Static Methods [#static-methods] `BusinessCalendar.new_custom(holidays, weekmask)` [#businesscalendarnew_customholidays-weekmask] Create a custom calendar with explicit holidays and weekend mask. | Name | Type | Default | Description | | ---------- | ----------- | ---------- | ----------------------------------------------- | | `holidays` | `list[str]` | *required* | Holiday dates as `"YYYY-MM-DD"` strings | | `weekmask` | `list[int]` | *required* | Weekend day numbers (`0`=Mon, `5`=Sat, `6`=Sun) | Methods [#methods-1] | Method | Returns | Description | | ---------------------------- | ------------------ | ----------------------------------------------------------------- | | `.is_holiday(date)` | `bool` | Check if a date is a holiday | | `.is_business_day(date)` | `bool` | Check if a date is a business day | | `.bus_day_count(start, end)` | `int` | Count business days between start (inclusive) and end (exclusive) | | `.union_cal(other)` | `BusinessCalendar` | Create a union calendar (holiday from either calendar) | Example [#example-1] ```python import datetime from vade import BusinessCalendar nyc = BusinessCalendar("NYC") nyc.is_business_day(datetime.date(2024, 12, 25)) # False nyc.is_holiday(datetime.date(2024, 12, 25)) # True nyc.bus_day_count(datetime.date(2024, 1, 1), datetime.date(2024, 1, 31)) # 20 ldn = BusinessCalendar("LDN") combined = nyc.union_cal(ldn) combined.is_business_day(datetime.date(2024, 12, 26)) # False (Boxing Day in LDN) nyc.is_business_day(datetime.date(2024, 12, 26)) # True (not a NYC holiday) custom = BusinessCalendar.new_custom(["2024-03-01"], [5, 6]) custom.is_holiday(datetime.date(2024, 3, 1)) # True ``` dcf [#dcf] Compute the day count fraction between two dates. ```bash dcf(convention, start, end, calendar=None, termination=None) ``` Parameters [#parameters-2] | Name | Type | Default | Description | | ------------- | ---------------------------- | ---------- | ---------------------------------------------------------------- | | `convention` | `str` | *required* | Day count convention (e.g., `"act360"`, `"act365f"`, `"30/360"`) | | `start` | `datetime.date` | *required* | Start date | | `end` | `datetime.date` | *required* | End date | | `calendar` | `BusinessCalendar` or `None` | `None` | Required for `"bus252"` convention | | `termination` | `datetime.date` or `None` | `None` | Required for `"30e/360isda"` convention | See [Conventions](../conventions#day-count-conventions) for all accepted conventions. Example [#example-2] ```python import datetime from vade import dcf start = datetime.date(2024, 1, 15) end = datetime.date(2024, 7, 15) round(dcf("act360", start, end), 10) # 0.5055555556 round(dcf("act365f", start, end), 10) # 0.498630137 ``` add_tenor [#add_tenor] Add a tenor string to a date. ```bash add_tenor(date, tenor, calendar=None) ``` Parameters [#parameters-3] | Name | Type | Default | Description | | ---------- | ---------------------------- | ---------- | ----------------------------------------------------------- | | `date` | `datetime.date` | *required* | Base date | | `tenor` | `str` | *required* | Tenor string (e.g., `"2Y"`, `"6M"`, `"1W"`, `"3D"`, `"3B"`) | | `calendar` | `BusinessCalendar` or `None` | `None` | Required for business day tenors (`"NB"`) | See [Conventions](../conventions#tenor-strings) for tenor string format. Example [#example-3] ```python import datetime from vade import add_tenor, BusinessCalendar base = datetime.date(2024, 1, 15) add_tenor(base, "6M") # datetime.date(2024, 7, 15) add_tenor(base, "1Y") # datetime.date(2025, 1, 15) add_tenor(base, "3B", BusinessCalendar("NYC")) # datetime.date(2024, 1, 18) ``` add_business_days [#add_business_days] Add or subtract business days from a date. ```bash add_business_days(date, days, calendar) ``` Parameters [#parameters-4] | Name | Type | Default | Description | | ---------- | ------------------ | ---------- | ------------------------------------------------------------- | | `date` | `datetime.date` | *required* | Base date | | `days` | `int` | *required* | Number of business days (positive forward, negative backward) | | `calendar` | `BusinessCalendar` | *required* | Business day calendar | Example [#example-4] ```python import datetime from vade import add_business_days, BusinessCalendar nyc = BusinessCalendar("NYC") base = datetime.date(2024, 1, 15) add_business_days(base, 2, nyc) # datetime.date(2024, 1, 17) add_business_days(base, -2, nyc) # datetime.date(2024, 1, 11) ``` # Cashflows (/docs/api/cashflows) Fixed and floating rate legs, period types, cashflow rows, and DataFrame conversion utilities. Related: [Pricing Guide](../guides/rates/pricing) demonstrates cashflow analysis with `.cashflows()` DataFrame output. Leg types are available via flat import: ```bash from vade import FixedLeg, FloatLeg from vade.cashflows import ( ZeroFixedLeg, ZeroFloatLeg, CustomLeg, CashflowRow, cashflows_to_polars, cashflows_to_pandas, FixedPeriod, FloatPeriod, IborPeriod, ZeroFixedPeriod, ZeroFloatPeriod, CashflowPeriod, ) ``` See [Conventions](../conventions) for all accepted string parameter values. **Contents:** [FixedLeg](#fixedleg) | [FloatLeg](#floatleg) | [ZeroFixedLeg](#zerofixedleg) | [ZeroFloatLeg](#zerofloatleg) | [CustomLeg](#customleg) | [CashflowRow](#cashflowrow) | [cashflows\_to\_polars](#cashflows_to_polars) | [cashflows\_to\_pandas](#cashflows_to_pandas) | [Period Types](#period-types) FixedLeg [#fixedleg] Fixed-rate leg constructed from a [Schedule](calendar#schedule). *Rust-backed.* Constructor [#constructor] ```bash FixedLeg(schedule, fixed_rate, notional, convention="act360", currency="USD", payment_lag=None, amortization=None) ``` Parameters [#parameters] | Name | Type | Default | Description | | -------------- | ---------------------------- | ---------- | -------------------------------------------------------------------------------------------- | | `schedule` | `Schedule` | *required* | Payment schedule (from [Schedule](calendar#schedule)) | | `fixed_rate` | `float` | *required* | Fixed rate in percent (e.g., 3.0 for 3%) | | `notional` | `float` | *required* | Notional amount (negative = pay, positive = receive) | | `convention` | `str` | `"act360"` | Day count convention | | `currency` | `str` | `"USD"` | Currency code | | `payment_lag` | `int \| None` | `None` | Payment delay in business days | | `amortization` | `str \| list[float] \| None` | `None` | Amortization schedule: `None`, `"constant_AMOUNT"`, `"percentage_PCT"`, or list of notionals | See [Conventions](../conventions#day-count-conventions) for day count convention values. Properties [#properties] | Property | Type | Description | | ------------ | ----- | ----------------- | | `.n_periods` | `int` | Number of periods | Methods [#methods] | Method | Returns | Description | | ------------------------ | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `.npv(curve)` | `float \| Dual` | Net present value of all cashflows (see the [type system guide](../getting-started/type-system#union-return-types) for details on Dual return types) | | `.analytic_delta(curve)` | `float \| Dual` | Analytic delta (DV01-like sensitivity) | | `.cashflows(curve=None)` | `list[CashflowRow]` | Cashflow detail rows; includes notional exchanges. DF/NPV populated if curve provided | Example [#example] ```python import datetime from vade import FixedLeg, Schedule, DiscountCurve from vade.cashflows import cashflows_to_polars schedule = Schedule( effective=datetime.date(2024, 1, 1), termination=datetime.date(2025, 1, 1), frequency="Q", ) leg = FixedLeg(schedule, fixed_rate=3.0, notional=1_000_000.0) leg.n_periods # 4 rows = leg.cashflows() len(rows) > 0 # True nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97} curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") npv = leg.npv(curve) isinstance(npv, float) # True df = cashflows_to_polars(leg.cashflows(curve)) "Cashflow" in df.columns # True "DF" in df.columns # True "NPV" in df.columns # True ``` FloatLeg [#floatleg] Floating-rate leg with RFR compounding constructed from a [Schedule](calendar#schedule). *Rust-backed.* Constructor [#constructor-1] ```bash FloatLeg( schedule, notional, calendar, spread=0.0, convention="act360", fixing_method="rfr_payment_delay", spread_method="none_simple", currency="USD", payment_lag=None, amortization=None, ) ``` Parameters [#parameters-1] | Name | Type | Default | Description | | --------------- | ---------------------------- | --------------------- | ------------------------------------------------------------------ | | `schedule` | `Schedule` | *required* | Payment schedule (from [Schedule](calendar#schedule)) | | `notional` | `float` | *required* | Notional amount (negative = pay, positive = receive) | | `calendar` | `BusinessCalendar` | *required* | Business day calendar for fixing date computation | | `spread` | `float` | `0.0` | Spread in percent (e.g., 0.5 for 50bp) | | `convention` | `str` | `"act360"` | Day count convention | | `fixing_method` | `str` | `"rfr_payment_delay"` | RFR fixing method (see [Conventions](../conventions)) | | `spread_method` | `str` | `"none_simple"` | Spread compounding method: `"none_simple"` or `"isda_compounding"` | | `currency` | `str` | `"USD"` | Currency code | | `payment_lag` | `int \| None` | `None` | Payment delay in business days | | `amortization` | `str \| list[float] \| None` | `None` | Amortization schedule | See [BusinessCalendar](calendar#businesscalendar) for calendar construction. Properties [#properties-1] | Property | Type | Description | | ------------ | ----- | ----------------- | | `.n_periods` | `int` | Number of periods | Methods [#methods-1] | Method | Returns | Description | | -------------------------------------- | ------------------- | --------------------------------------------------------- | | `.npv(curve, fixings=None)` | `float \| Dual` | Net present value; pass historical fixings dict if needed | | `.analytic_delta(curve)` | `float \| Dual` | Analytic delta (DV01-like sensitivity) | | `.cashflows(curve=None, fixings=None)` | `list[CashflowRow]` | Cashflow detail rows; DF/NPV populated if curve provided | The `fixings` parameter is a `dict[datetime.date, float]` mapping fixing dates to observed rates. Example [#example-1] ```python import datetime from vade import FloatLeg, Schedule, DiscountCurve, BusinessCalendar from vade.cashflows import cashflows_to_polars schedule = Schedule( effective=datetime.date(2024, 1, 1), termination=datetime.date(2025, 1, 1), frequency="Q", ) cal = BusinessCalendar.new_custom([], [5, 6]) leg = FloatLeg(schedule, notional=1_000_000.0, calendar=cal, spread=0.5) leg.n_periods # 4 nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97} curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") npv = leg.npv(curve) isinstance(npv, float) # True rows = leg.cashflows(curve) df = cashflows_to_polars(rows) "Cashflow" in df.columns # True ``` ZeroFixedLeg [#zerofixedleg] Zero-coupon fixed-rate leg with a single accrual period. *Rust-backed.* Constructor [#constructor-2] ```bash ZeroFixedLeg(start, end, payment, fixed_rate, notional, convention="act360", currency="USD") ``` Parameters [#parameters-2] | Name | Type | Default | Description | | ------------ | --------------- | ---------- | --------------------- | | `start` | `datetime.date` | *required* | Accrual start date | | `end` | `datetime.date` | *required* | Accrual end date | | `payment` | `datetime.date` | *required* | Payment date | | `fixed_rate` | `float` | *required* | Fixed rate in percent | | `notional` | `float` | *required* | Notional amount | | `convention` | `str` | `"act360"` | Day count convention | | `currency` | `str` | `"USD"` | Currency code | Methods [#methods-2] | Method | Returns | Description | | ------------------------ | ------------------- | -------------------- | | `.npv(curve)` | `float \| Dual` | Net present value | | `.analytic_delta(curve)` | `float \| Dual` | Analytic delta | | `.cashflows(curve=None)` | `list[CashflowRow]` | Cashflow detail rows | Example [#example-2] ```python import datetime from vade.cashflows import ZeroFixedLeg leg = ZeroFixedLeg( start=datetime.date(2024, 1, 1), end=datetime.date(2025, 1, 1), payment=datetime.date(2025, 1, 1), fixed_rate=3.0, notional=1_000_000.0, ) rows = leg.cashflows() len(rows) > 0 # True ``` ZeroFloatLeg [#zerofloatleg] Zero-coupon floating-rate leg with RFR compounding. *Rust-backed.* Constructor [#constructor-3] ```bash ZeroFloatLeg( start, end, payment, notional, calendar, spread=0.0, convention="act360", fixing_method="rfr_payment_delay", spread_method="none_simple", currency="USD", ) ``` Parameters [#parameters-3] | Name | Type | Default | Description | | --------------- | ------------------ | --------------------- | -------------------------------------- | | `start` | `datetime.date` | *required* | Accrual start date | | `end` | `datetime.date` | *required* | Accrual end date | | `payment` | `datetime.date` | *required* | Payment date | | `notional` | `float` | *required* | Notional amount | | `calendar` | `BusinessCalendar` | *required* | Business day calendar for fixing dates | | `spread` | `float` | `0.0` | Spread in percent | | `convention` | `str` | `"act360"` | Day count convention | | `fixing_method` | `str` | `"rfr_payment_delay"` | RFR fixing method | | `spread_method` | `str` | `"none_simple"` | Spread compounding method | | `currency` | `str` | `"USD"` | Currency code | Methods [#methods-3] | Method | Returns | Description | | -------------------------------------- | ------------------- | -------------------- | | `.npv(curve, fixings=None)` | `float \| Dual` | Net present value | | `.analytic_delta(curve)` | `float \| Dual` | Analytic delta | | `.cashflows(curve=None, fixings=None)` | `list[CashflowRow]` | Cashflow detail rows | Example [#example-3] ```python import datetime from vade import BusinessCalendar from vade.cashflows import ZeroFloatLeg cal = BusinessCalendar.new_custom([], [5, 6]) leg = ZeroFloatLeg( start=datetime.date(2024, 1, 1), end=datetime.date(2025, 1, 1), payment=datetime.date(2025, 1, 1), notional=1_000_000.0, calendar=cal, ) rows = leg.cashflows() len(rows) > 0 # True ``` CustomLeg [#customleg] Leg with arbitrary heterogeneous period types. *Rust-backed.* Constructor [#constructor-4] ```bash CustomLeg(periods, currency="USD") ``` Parameters [#parameters-4] | Name | Type | Default | Description | | ---------- | -------------------------------------------------------------------------------------------------------- | ---------- | ------------------------------------ | | `periods` | `list[FixedPeriod \| FloatPeriod \| IborPeriod \| ZeroFixedPeriod \| ZeroFloatPeriod \| CashflowPeriod]` | *required* | Heterogeneous list of period objects | | `currency` | `str` | `"USD"` | Currency code | Properties [#properties-2] | Property | Type | Description | | ------------ | ----- | ----------------- | | `.n_periods` | `int` | Number of periods | Methods [#methods-4] | Method | Returns | Description | | -------------------------------------- | ------------------- | -------------------- | | `.npv(curve, fixings=None)` | `float \| Dual` | Net present value | | `.analytic_delta(curve)` | `float \| Dual` | Analytic delta | | `.cashflows(curve=None, fixings=None)` | `list[CashflowRow]` | Cashflow detail rows | Example [#example-4] ```python import datetime from vade.cashflows import CustomLeg, FixedPeriod, CashflowPeriod fp = FixedPeriod( start=datetime.date(2024, 1, 1), end=datetime.date(2024, 7, 1), payment=datetime.date(2024, 7, 1), notional=1_000_000.0, fixed_rate=3.0, ) cp = CashflowPeriod( payment=datetime.date(2025, 1, 1), notional=-1_000_000.0, ) leg = CustomLeg(periods=[fp, cp]) leg.n_periods # 2 rows = leg.cashflows() len(rows) # 2 ``` CashflowRow [#cashflowrow] A single row of cashflow data returned by leg `.cashflows()` methods. *Rust-backed.* Not constructed directly -- returned by `FixedLeg.cashflows()`, `FloatLeg.cashflows()`, and other leg types. Properties [#properties-3] | Property | Type | Description | | -------------- | -------------------------------- | ----------------------------------------------------------------- | | `.period_type` | `str` | Period type identifier (e.g., `"Fixed"`, `"Float"`, `"Cashflow"`) | | `.start` | `datetime.date` | Accrual start date | | `.end` | `datetime.date` | Accrual end date | | `.payment` | `datetime.date` | Payment date | | `.currency` | `str` | Currency code | | `.notional` | `float` | Period notional | | `.fixing_rate` | `float \| Dual \| Dual2 \| None` | Observed/projected fixing rate | | `.rate` | `float` | Effective rate for the period | | `.spread` | `float` | Spread applied | | `.dcf` | `float` | Day count fraction | | `.cashflow` | `float \| Dual \| Dual2` | Period cashflow amount | | `.df` | `float \| Dual \| Dual2 \| None` | Discount factor (None if no curve provided) | | `.npv` | `float \| Dual \| Dual2 \| None` | Discounted cashflow (None if no curve provided) | See [Dual](autodiff#dual) and [Dual2](autodiff#dual2) for automatic differentiation types. Example [#example-5] ```python import datetime from vade import FixedLeg, Schedule schedule = Schedule( effective=datetime.date(2024, 1, 1), termination=datetime.date(2025, 1, 1), frequency="Q", ) leg = FixedLeg(schedule, fixed_rate=3.0, notional=1_000_000.0) rows = leg.cashflows() # Examine a fixed-rate period (skip initial notional exchange) fixed_rows = [r for r in rows if r.period_type == "Fixed"] row = fixed_rows[0] row.period_type # 'Fixed' isinstance(row.start, datetime.date) # True isinstance(row.end, datetime.date) # True row.rate # 0.03 row.dcf > 0 # True isinstance(row.cashflow, float) # True row.df is None # True (no curve provided) row.npv is None # True (no curve provided) ``` cashflows_to_polars [#cashflows_to_polars] Convert a list of [CashflowRow](#cashflowrow) to a Polars DataFrame. ```bash cashflows_to_polars(rows: list[CashflowRow]) -> polars.DataFrame ``` Parameters [#parameters-5] | Name | Type | Default | Description | | ------ | ------------------- | ---------- | -------------------------------------------------- | | `rows` | `list[CashflowRow]` | *required* | Cashflow rows from any leg's `.cashflows()` method | DataFrame Columns [#dataframe-columns] | Column | Type | Description | | ------------- | ------ | ------------------------------------------------------ | | `Type` | `str` | Period type (`"Fixed"`, `"Float"`, `"Cashflow"`, etc.) | | `start` | `date` | Accrual start date | | `end` | `date` | Accrual end date | | `payment` | `date` | Payment date | | `Ccy` | `str` | Currency code | | `Notional` | `f64` | Period notional | | `fixing_rate` | `f64` | Fixing rate (null if not applicable) | | `Rate` | `f64` | Effective rate | | `Spread` | `f64` | Spread | | `DCF` | `f64` | Day count fraction | | `Cashflow` | `f64` | Cashflow amount | | `DF` | `f64` | Discount factor (only if curve was provided) | | `NPV` | `f64` | Discounted cashflow (only if curve was provided) | Example [#example-6] ```python import datetime from vade import FixedLeg, Schedule, DiscountCurve from vade.cashflows import cashflows_to_polars schedule = Schedule( effective=datetime.date(2024, 1, 1), termination=datetime.date(2025, 1, 1), frequency="Q", ) leg = FixedLeg(schedule, fixed_rate=3.0, notional=1_000_000.0) # Without curve: 11 columns rows = leg.cashflows() df = cashflows_to_polars(rows) len(df.columns) # 11 "Cashflow" in df.columns # True # With curve: 13 columns (adds DF, NPV) nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97} curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") rows_with_curve = leg.cashflows(curve) df2 = cashflows_to_polars(rows_with_curve) len(df2.columns) # 13 "DF" in df2.columns # True "NPV" in df2.columns # True ``` cashflows_to_pandas [#cashflows_to_pandas] Convert a list of [CashflowRow](#cashflowrow) to a pandas DataFrame. Same columns as [`cashflows_to_polars`](#cashflows_to_polars). ```bash cashflows_to_pandas(rows: list[CashflowRow]) -> pandas.DataFrame ``` Parameters [#parameters-6] | Name | Type | Default | Description | | ------ | ------------------- | ---------- | -------------------------------------------------- | | `rows` | `list[CashflowRow]` | *required* | Cashflow rows from any leg's `.cashflows()` method | Example [#example-7] ```python import datetime from vade import FixedLeg, Schedule from vade.cashflows import cashflows_to_pandas schedule = Schedule( effective=datetime.date(2024, 1, 1), termination=datetime.date(2025, 1, 1), frequency="Q", ) leg = FixedLeg(schedule, fixed_rate=3.0, notional=1_000_000.0) rows = leg.cashflows() df = cashflows_to_pandas(rows) "Cashflow" in df.columns # True ``` Period Types [#period-types] Low-level accrual period building blocks. Rarely constructed directly -- legs create these internally. | Type | Description | Key Parameters | | ----------------- | ------------------------------- | ------------------------------------------------------------------------------------------- | | `FixedPeriod` | Fixed-rate accrual period | start, end, payment, notional, fixed\_rate, convention | | `FloatPeriod` | RFR floating-rate period | start, end, payment, notional, calendar, spread, convention, fixing\_method, spread\_method | | `IborPeriod` | IBOR fixing period | start, end, payment, notional, calendar, spread, convention, fixing\_lag | | `ZeroFixedPeriod` | Zero-coupon fixed period | start, end, payment, notional, fixed\_rate, convention | | `ZeroFloatPeriod` | Zero-coupon floating period | start, end, payment, notional, calendar, spread, convention, fixing\_method, spread\_method | | `CashflowPeriod` | Simple notional exchange or fee | payment, notional, currency | All period types have `.npv(curve)` method. Float and IBOR periods also accept a `fixings` parameter. `FixedPeriod` and `ZeroFixedPeriod` also have `.cashflow()` returning the undiscounted cashflow amount. Period types are used with [CustomLeg](#customleg) when you need heterogeneous periods in a single leg. # Market Context (/docs/api/context) Market data container, fixing store, and evaluation date management for production pricing workflows. See also: [Market Context Guide](../guides/market-context) for complete usage patterns and serialization workflows. All types are available via flat import: ```bash from vade import MarketContext, FixingStore, eval_date ``` **Contents:** [FixingStore](#fixingstore) | [MarketContext](#marketcontext) | [eval\_date](#eval_date) *** FixingStore [#fixingstore] In-memory store for historical fixing rates keyed by index name (e.g. "SOFR", "EURIBOR"). *Pure Python.* Constructor [#constructor] ```bash FixingStore() ``` Methods [#methods] | Method | Returns | Description | | ------------------------------ | --------------------------- | ----------------------------------------------------------------------------------- | | `.set(index, fixings)` | `None` | Store or merge fixings for an index. `fixings` is `dict[date, float]` | | `.get(index)` | `dict[date, float] \| None` | Get all fixings for an index (returns a copy) | | `.get_rate(index, dt)` | `float \| None` | Get single fixing rate by index and date | | `.clear(index=None)` | `None` | Clear one index or all indices if `None` | | `.list_indices()` | `list[str]` | Sorted list of stored index names | | `.from_polars(df, index=None)` | `None` | Bulk-load from Polars DataFrame with columns `date`, `rate`, and optionally `index` | | `.from_csv(path, index=None)` | `None` | Bulk-load from CSV file (same column format as `from_polars`) | | `len(store)` | `int` | Total count of all fixings across all indices | Example [#example] ```python import datetime from vade import FixingStore store = FixingStore() store.set("SOFR", { datetime.date(2024, 1, 2): 0.053, datetime.date(2024, 1, 3): 0.0531, datetime.date(2024, 1, 4): 0.0529, }) rate = store.get_rate("SOFR", datetime.date(2024, 1, 2)) assert rate == 0.053 assert store.list_indices() == ["SOFR"] assert len(store) == 3 # Merge additional fixings store.set("EURIBOR", {datetime.date(2024, 1, 2): 0.039}) assert store.list_indices() == ["EURIBOR", "SOFR"] assert len(store) == 4 ``` *** MarketContext [#marketcontext] Immutable container bundling all pricing inputs: evaluation date, named curves, historical fixings, FX rates, and calendars. *Pure Python.* Constructor [#constructor-1] ```bash MarketContext(*, eval_date=None, curves=None, fixings=None, fx_rates=None, fx_pairs=None, calendars=None) ``` Parameters [#parameters] | Name | Type | Default | Description | | ----------- | -------------------------- | ------- | ---------------------------------------------------------------------------------------------- | | `eval_date` | `datetime.date \| None` | `None` | Evaluation (pricing) date. Falls back to module-level `eval_date` context, then `date.today()` | | `curves` | `dict[str, Any] \| None` | `None` | Named curves keyed by string ID (e.g. `{"USD_SOFR": curve}`) | | `fixings` | `FixingStore \| None` | `None` | Historical fixing rates | | `fx_rates` | `FXRates \| None` | `None` | FX rate object for cross-currency pricing | | `fx_pairs` | `dict[str, float] \| None` | `None` | Raw FX pair dict for serialization (required if `fx_rates` provided) | | `calendars` | `dict[str, Any] \| None` | `None` | Named business day calendars | Properties [#properties] | Property | Type | Description | | ------------ | ----------------- | -------------------------------------------------- | | `.eval_date` | `datetime.date` | Read-only evaluation date | | `.curves` | `dict[str, Any]` | Read-only curves dict (empty dict if None) | | `.fixings` | `FixingStore` | Read-only fixing store (empty FixingStore if None) | | `.fx_rates` | `FXRates \| None` | Read-only FX rates object | | `.calendars` | `dict[str, Any]` | Read-only calendars dict (empty dict if None) | Methods: Construction [#methods-construction] | Method | Returns | Description | | ----------------------------------------------------------------------------------------------------- | --------------- | ------------------------------------------------------------------ | | `.from_solver(solver, *, eval_date=None, fixings=None, fx_rates=None, fx_pairs=None, calendars=None)` | `MarketContext` | Create context from calibrated Solver, extracting all curves by ID | Methods: Copy-on-Modify [#methods-copy-on-modify] | Method | Returns | Description | | -------------------------------------------- | --------------- | ------------------------------------------ | | `.with_eval_date(eval_date)` | `MarketContext` | New context with different evaluation date | | `.with_curve(curve_id, curve)` | `MarketContext` | New context with added/replaced curve | | `.with_fixings(fixings)` | `MarketContext` | New context with replaced fixing store | | `.with_fx_rates(fx_rates, *, fx_pairs=None)` | `MarketContext` | New context with replaced FX rates | | `.with_calendar(name, calendar)` | `MarketContext` | New context with added/replaced calendar | Methods: Lookup [#methods-lookup] | Method | Returns | Description | | ------------------ | -------------------------------------- | ------------------------------------------------------------ | | `.curve(curve_id)` | `DiscountCurve \| ForwardCurve \| ...` | Retrieve curve by string ID (raises `KeyError` if not found) | Methods: Analytics [#methods-analytics] | Method | Returns | Description | | ------------------------------------- | ------------------------ | ------------------------------------- | | `.discount_factor(curve_id, dt)` | `float \| Dual \| Dual2` | Discount factor from curve at date | | `.forward_rate(curve_id, start, end)` | `float \| Dual \| Dual2` | Forward rate from curve between dates | | `.zero_rate(curve_id, start, end)` | `float \| Dual \| Dual2` | Zero rate from curve between dates | | `.fx_rate(base, quote)` | `float` | FX spot rate for currency pair | Methods: Comparison [#methods-comparison] | Method | Returns | Description | | ------------------------------------- | ------------------- | -------------------------------------------------------------------- | | `.diff(other, *, as_dataframe=False)` | `dict \| DataFrame` | Compare two contexts: shows added/removed/changed curves and fixings | Methods: Serialization [#methods-serialization] | Method | Returns | Description | | ------------------------------------ | --------------- | ---------------------------------------------------------------- | | `.to_json()` | `str` | Serialize full context to JSON string | | `.from_json(json_str)` (classmethod) | `MarketContext` | Restore context from JSON string with polymorphic curve handling | Example [#example-1] ```python import datetime from vade import MarketContext, FixingStore, DiscountCurve effective = datetime.date(2025, 6, 16) curve = DiscountCurve( { effective: 1.0, datetime.date(2026, 6, 16): 0.9615, datetime.date(2027, 6, 16): 0.9246, }, interpolation="log_linear", convention="act360", id="usd_sofr", ) store = FixingStore() store.set("SOFR", {datetime.date(2025, 6, 13): 0.043}) ctx = MarketContext( eval_date=effective, curves={"usd_sofr": curve}, fixings=store, ) # Lookup assert ctx.curve("usd_sofr") is curve # Analytics df = ctx.discount_factor("usd_sofr", datetime.date(2026, 6, 16)) assert abs(df - 0.9615) < 0.001 # Copy-on-modify (original unchanged) ctx2 = ctx.with_eval_date(datetime.date(2025, 7, 16)) assert ctx.eval_date == effective assert ctx2.eval_date == datetime.date(2025, 7, 16) ``` *** eval_date [#eval_date] Context manager for temporarily setting the module-level default evaluation date. *Pure Python.* Usage [#usage] ```bash from vade import eval_date with eval_date(datetime.date(2025, 6, 16)): # All vade calls within this block use 2025-06-16 as default eval_date ... ``` Example [#example-2] ```python import datetime from vade import eval_date d = datetime.date(2025, 6, 16) with eval_date(d): pass # eval_date context active # context restored after block exits ``` # API Reference (/docs/api) Complete parameter signatures, types, and working examples for every vade class and function. *** Product Sections [#product-sections] * [Rates](./rates/) -- Interest rate curves, instruments, and calibration * [FX](./fx/) -- FX rates, forwards, cross-currency, and non-deliverable instruments * [Credit](./credit/) -- Bonds, CDS, callable bonds, and spread analytics Shared Infrastructure [#shared-infrastructure] * [Calendar](./calendar) -- Schedule generation, business day calendars, day count fractions, tenor arithmetic * [Autodiff](./autodiff) -- Dual and Dual2 automatic differentiation types * [Numerical](./numerical) -- Root-finding solvers (Brent, Newton-Raphson) * [Context](./context) -- MarketContext, FixingStore, and evaluation date management * [Cashflows](./cashflows) -- Fixed and floating leg cashflow generation # Numerical (/docs/api/numerical) Root-finding solvers implemented in Rust for high performance. Both methods find x such that f(x) = 0. Related: [Bootstrap & Parametric Guide](../guides/rates/bootstrap-parametric) uses Brent root-finding for curve stripping. brent_solve [#brent_solve] Brent's method root finder. Combines bisection, secant, and inverse quadratic interpolation for robust convergence. Requires a bracketing interval where f(a) and f(b) have opposite signs. ```bash result = brent_solve(f, a, b, tol=1e-12, max_iter=200) ``` | Name | Type | Default | Description | | ---------- | -------------------------- | -------- | ------------------------------------------------------ | | `f` | `Callable[[float], float]` | required | Function to find root of | | `a` | `float` | required | Lower bracket (f(a) and f(b) must have opposite signs) | | `b` | `float` | required | Upper bracket | | `tol` | `float` | `1e-12` | Convergence tolerance | | `max_iter` | `int` | `200` | Maximum iterations | **Returns:** `float` -- the root x where f(x) is approximately 0. **Raises:** `ValueError` if root is not bracketed (f(a) and f(b) have the same sign) or maximum iterations exceeded. Example [#example] ```python from vade import brent_solve # Find x where x^2 - 2 = 0 (i.e., sqrt(2)) root = brent_solve(lambda x: x**2 - 2, 1.0, 2.0) print(round(root, 10)) # 1.4142135624 ``` newton_raphson_solve [#newton_raphson_solve] Newton-Raphson root finder. Uses the derivative for quadratic convergence. The callable must return both the function value and its derivative as a tuple. ```bash result = newton_raphson_solve(f, x0, tol=1e-12, max_iter=200) ``` | Name | Type | Default | Description | | ---------- | ---------------------------------------- | -------- | -------------------------------------------- | | `f` | `Callable[[float], Tuple[float, float]]` | required | Function returning (value, derivative) tuple | | `x0` | `float` | required | Initial guess | | `tol` | `float` | `1e-12` | Convergence tolerance | | `max_iter` | `int` | `200` | Maximum iterations | **Returns:** `float` -- the root x where f(x) is approximately 0. **Raises:** `ValueError` if derivative is zero or maximum iterations exceeded. Example [#example-1] ```python from vade import newton_raphson_solve # Find x where x^2 - 2 = 0, providing f(x) and f'(x) = 2x root = newton_raphson_solve(lambda x: (x**2 - 2, 2 * x), 1.5) print(round(root, 10)) # 1.4142135624 ``` Choosing a Solver [#choosing-a-solver] For functions where analytic derivatives are available, `newton_raphson_solve` typically converges faster (quadratic vs superlinear). For functions without easy derivatives, use `brent_solve` with a bracketing interval. See [Autodiff](autodiff) for automatic differentiation types that can compute derivatives for use with `newton_raphson_solve`. # Calibration (/docs/guides/calibration) Calibrate single and multi-curve frameworks using the [Solver](../api/rates/solver#solver) with Levenberg-Marquardt, Gauss-Newton, and gradient descent algorithms. Single-Curve Calibration [#single-curve-calibration] Calibrate a single USD SOFR [DiscountCurve](../api/rates/curves#discountcurve) using the default Levenberg-Marquardt algorithm. Define curve nodes at instrument maturity dates with initial discount factors of 1.0, then let the solver find the correct values. ```python import datetime from vade import DiscountCurve, IRS, Solver effective = datetime.date(2025, 6, 16) nodes = { effective: 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, datetime.date(2035, 6, 16): 1.0, } curve = DiscountCurve( nodes, interpolation="log_linear", convention="act360", id="sofr" ) irs_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=4.00, convention="act360", float_convention="act360") irs_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") irs_3y = IRS(effective=effective, termination="3Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360") irs_5y = IRS(effective=effective, termination="5Y", frequency="a", fixed_rate=3.70, convention="act360", float_convention="act360") irs_10y = IRS(effective=effective, termination="10Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") instruments = [ (irs_1y, 4.00), (irs_2y, 3.85), (irs_3y, 3.75), (irs_5y, 3.70), (irs_10y, 3.85), ] solver = Solver(curves=[curve], instruments=instruments) result = solver.iterate() result.converged # True result.iterations > 0 # True ``` Solver Diagnostics [#solver-diagnostics] Inspect calibration quality through the [SolverResult](../api/rates/solver#solverresult) diagnostics. Residuals show per-instrument calibration error and the Jacobian reveals sensitivity structure. ```python import datetime from vade import DiscountCurve, IRS, Solver effective = datetime.date(2025, 6, 16) nodes = { effective: 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, datetime.date(2035, 6, 16): 1.0, } curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360", id="sofr") irs_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=4.00, convention="act360", float_convention="act360") irs_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") irs_3y = IRS(effective=effective, termination="3Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360") irs_5y = IRS(effective=effective, termination="5Y", frequency="a", fixed_rate=3.70, convention="act360", float_convention="act360") irs_10y = IRS(effective=effective, termination="10Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") solver = Solver( curves=[curve], instruments=[(irs_1y, 4.00), (irs_2y, 3.85), (irs_3y, 3.75), (irs_5y, 3.70), (irs_10y, 3.85)], ) result = solver.iterate() result.objective < 1e-10 # True max(abs(r) for r in result.residuals) < 1e-8 # True result.jacobian.shape # (5, 5) result.condition_number > 0 # True calibrated = solver.get_curve(0) float(calibrated.discount_factor(datetime.date(2026, 6, 16))) < 1.0 # True float(calibrated.discount_factor(datetime.date(2035, 6, 16))) > 0.5 # True float(calibrated.zero_rate(effective, datetime.date(2026, 6, 16))) > 0.03 # True ``` Multi-Curve Calibration [#multi-curve-calibration] Separate OIS discounting from forward projection using `pre_solvers`. The inner solver calibrates the OIS discount curve, and the outer solver calibrates the forward curve while using the pre-calibrated discount curve for present-value calculations. ```python import datetime from vade import DiscountCurve, IRS, Solver effective = datetime.date(2025, 6, 16) # Stage 1: OIS discount curve (slightly lower rates than Term SOFR) disc_nodes = { effective: 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, } disc_curve = DiscountCurve( disc_nodes, interpolation="log_linear", convention="act360", id="ois_disc" ) ois_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=3.90, convention="act360", float_convention="act360") ois_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360") ois_3y = IRS(effective=effective, termination="3Y", frequency="a", fixed_rate=3.65, convention="act360", float_convention="act360") inner_solver = Solver( curves=[disc_curve], instruments=[(ois_1y, 3.90), (ois_2y, 3.75), (ois_3y, 3.65)], ) # Stage 2: Forward projection curve fwd_nodes = { effective: 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, } fwd_curve = DiscountCurve( fwd_nodes, interpolation="log_linear", convention="act360", id="sofr_fwd" ) fwd_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=4.00, convention="act360", float_convention="act360") fwd_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") fwd_3y = IRS(effective=effective, termination="3Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360") outer_solver = Solver( curves=[fwd_curve], instruments=[(fwd_1y, 4.00), (fwd_2y, 3.85), (fwd_3y, 3.75)], pre_solvers=[inner_solver], ) mc_result = outer_solver.iterate() mc_result.converged # True mc_result.iterations > 0 # True ``` The dict [instrument format](../api/rates/solver#instruments-format) gives explicit control over which curves are used for discounting and forecasting. Use `disc_curve_idx` and `forecast_curve_idx` to map each instrument to the correct curve when a single solver holds multiple curves. ```python import datetime from vade import DiscountCurve, IRS, Solver effective = datetime.date(2025, 6, 16) disc_curve = DiscountCurve( {effective: 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0}, interpolation="log_linear", convention="act360", id="disc", ) fwd_curve = DiscountCurve( {effective: 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0}, interpolation="log_linear", convention="act360", id="fwd", ) irs_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=4.00, convention="act360", float_convention="act360") irs_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") instruments = [ {"instrument": irs_1y, "target": 4.00, "disc_curve_idx": 0, "forecast_curve_idx": 1, "label": "1Y_IRS", "currency": "USD"}, {"instrument": irs_2y, "target": 3.85, "disc_curve_idx": 0, "forecast_curve_idx": 1, "label": "2Y_IRS", "currency": "USD"}, ] solver = Solver(curves=[disc_curve, fwd_curve], instruments=instruments) result = solver.iterate() result.converged # True ``` Algorithm Comparison [#algorithm-comparison] Compare Levenberg-Marquardt, Gauss-Newton, and gradient descent on the same single-curve calibration problem. Each algorithm needs a fresh curve since the solver mutates node values in place. ```python import datetime from vade import DiscountCurve, IRS, Solver effective = datetime.date(2025, 6, 16) irs_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=4.00, convention="act360", float_convention="act360") irs_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") irs_3y = IRS(effective=effective, termination="3Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360") irs_5y = IRS(effective=effective, termination="5Y", frequency="a", fixed_rate=3.70, convention="act360", float_convention="act360") irs_10y = IRS(effective=effective, termination="10Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") instruments = [ (irs_1y, 4.00), (irs_2y, 3.85), (irs_3y, 3.75), (irs_5y, 3.70), (irs_10y, 3.85), ] def make_curve(): return DiscountCurve( { effective: 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, datetime.date(2035, 6, 16): 1.0, }, interpolation="log_linear", convention="act360", id="sofr", ) # Levenberg-Marquardt (default) solver_lm = Solver(curves=[make_curve()], instruments=instruments) result_lm = solver_lm.iterate() result_lm.converged # True result_lm.algorithm # 'levenberg_marquardt' # Gauss-Newton solver_gn = Solver(curves=[make_curve()], instruments=instruments, algorithm="gauss_newton") result_gn = solver_gn.iterate() result_gn.converged # True result_gn.algorithm # 'gauss_newton' # Gradient Descent solver_gd = Solver(curves=[make_curve()], instruments=instruments, algorithm="gradient_descent") result_gd = solver_gd.iterate() result_gd.converged # True result_gd.algorithm # 'gradient_descent' result_gd.iterations >= result_lm.iterations # True ``` Next Steps [#next-steps] * [Risk](rates/risk) -- compute delta, gamma, and bucket-level sensitivities from calibrated curves * [Quick Start](quick-start) -- see the full pipeline in one workflow # Market Context (/docs/guides/market-context) A `MarketContext` is an immutable container that bundles all pricing inputs into a single object: evaluation date, named curves, historical fixings, FX rates, and business calendars. Instead of threading `curves=`, `fixings=`, and `fx=` through every pricing call, you pass a single `ctx=` argument. This simplifies multi-curve workflows, makes serialization straightforward (one JSON blob captures the entire pricing environment), and eliminates a common source of errors where curves and fixings get out of sync. For complete method signatures and parameter details, see the [API reference](../api/context). Setup [#setup] Build a realistic USD SOFR discount curve via Solver calibration: ```python sofr_curve = DiscountCurve( { effective: 1.0, datetime.date(2025, 7, 16): 1.0, datetime.date(2025, 9, 16): 1.0, datetime.date(2025, 12, 16): 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, datetime.date(2035, 6, 16): 1.0, }, interpolation="log_linear", convention="act360", id="sofr", ) dep_1m = Deposit(effective=effective, termination="1m", rate=0.0, convention="act360") fra_3m = FRA(effective=effective, termination="3m", fixed_rate=0.0, convention="act360") fra_6m = FRA(effective=effective, termination="6m", fixed_rate=0.0, convention="act360") irs_1y = IRS(effective=effective, termination="1y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_2y = IRS(effective=effective, termination="2y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_3y = IRS(effective=effective, termination="3y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_5y = IRS(effective=effective, termination="5y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_10y = IRS(effective=effective, termination="10y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") instruments = [ (dep_1m, 0.0430), (fra_3m, 0.0425), (fra_6m, 0.0415), (irs_1y, 4.00), (irs_2y, 3.85), (irs_3y, 3.75), (irs_5y, 3.70), (irs_10y, 3.85), ] my_solver = Solver(curves=[sofr_curve], instruments=instruments) result = my_solver.iterate() assert result.converged sofr_curve = my_solver.get_curve(0) ``` Creating a Context [#creating-a-context] Create a `FixingStore` with historical SOFR fixings, then bundle everything into a `MarketContext`: ```python continuation store = FixingStore() store.set("SOFR", { datetime.date(2025, 6, 13): 0.0432, datetime.date(2025, 6, 12): 0.0431, datetime.date(2025, 6, 11): 0.0430, }) ctx = MarketContext( eval_date=effective, curves={"sofr": sofr_curve}, fixings=store, ) assert ctx.eval_date == effective assert "sofr" in ctx.curves assert ctx.fixings is store assert ctx.curve("sofr") is not None ``` From Solver [#from-solver] The production onramp is `MarketContext.from_solver()`. It extracts all curves from a calibrated Solver by their string IDs (set via the `id=` parameter on each curve). This is the recommended way to build a context after calibration: ```python continuation ctx2 = MarketContext.from_solver(my_solver, eval_date=effective, fixings=store) # Curves are extracted by their id= parameter ("sofr" in this case) assert "sofr" in ctx2.curves retrieved = ctx2.curve("sofr") assert retrieved is not None ``` Pricing with ctx= [#pricing-with-ctx] The key user benefit: pass `ctx=` instead of explicit `curves=`. The instrument resolves its curves from the context by `disc_curve_id`: ```python continuation irs = IRS( effective=effective, termination="5y", frequency="a", fixed_rate=0.037, convention="act360", float_convention="act360", disc_curve_id="sofr", ) # Explicit curve passing npv_explicit = irs.npv(curves=sofr_curve) # Context-based pricing -- same result, cleaner API npv_ctx = irs.npv(ctx=ctx2) assert abs(float(npv_explicit) - float(npv_ctx)) < 1e-8 ``` Copy-on-Modify [#copy-on-modify] `MarketContext` is immutable. The `with_*` methods return new contexts, leaving the original unchanged: ```python continuation ctx_bumped = ctx2.with_eval_date(datetime.date(2025, 7, 16)) assert ctx2.eval_date == effective assert ctx_bumped.eval_date == datetime.date(2025, 7, 16) ctx_extra = ctx2.with_curve("eur_curve", sofr_curve) assert "eur_curve" in ctx_extra.curves assert "eur_curve" not in ctx2.curves ``` Analytics [#analytics] Convenience query methods let you interrogate the context directly, without extracting individual curves: ```python continuation # Discount factor 1 year out df = ctx2.discount_factor("sofr", datetime.date(2026, 6, 16)) assert 0.9 < float(df) < 1.0 # 6-month forward rate fwd = ctx2.forward_rate("sofr", datetime.date(2025, 12, 16), datetime.date(2026, 6, 16)) assert 0.01 < float(fwd) < 0.08 # 2-year zero rate zr = ctx2.zero_rate("sofr", effective, datetime.date(2027, 6, 16)) assert 0.01 < float(zr) < 0.08 ``` Context Diffing [#context-diffing] Compare two contexts to see what changed. The `diff()` method returns a dict summarizing differences across all fields: ```python continuation ctx_shifted = ctx2.with_curve("sofr", sofr_curve.shift(10)) d = ctx_shifted.diff(ctx2) assert "eval_date" in d assert "curves" in d assert d["eval_date"]["changed"] is False assert "sofr" in d["curves"]["common"] ``` Serialization Round-Trip [#serialization-round-trip] Serialize a context to JSON and restore it. The restored context produces identical pricing results: ```python continuation json_str = ctx2.to_json() assert isinstance(json_str, str) assert len(json_str) > 0 ctx_restored = MarketContext.from_json(json_str) assert ctx_restored.eval_date == ctx2.eval_date assert "sofr" in ctx_restored.curves # Price the same IRS against the restored context npv_restored = irs.npv(ctx=ctx_restored) assert abs(float(npv_ctx) - float(npv_restored)) < 1e-8 ``` Next Steps [#next-steps] * [API Reference](../api/context) -- Complete method signatures and parameter details * [Serialization](serialization) -- JSON round-trips for curves and instruments # Quick Start (/docs/guides/quick-start) Build a USD SOFR discount curve, calibrate it to market instruments, price swaps, and compute risk -- all in one workflow. This guide uses [DiscountCurve](../api/rates/curves#discountcurve) for curve construction and [Solver](../api/rates/solver#solver) for calibration. *** Market Data [#market-data] We start with a set of USD SOFR market quotes observed on 16 June 2025. The dataset spans FRAs and interest rate swaps out to 10 years, with rates in the 3.7--4.3% range reflecting a post-hiking-cycle environment. All instruments use the [act360](../conventions#day-count-conventions) day count convention. ```python import datetime from vade import DiscountCurve, IRS, FRA, Solver effective = datetime.date(2025, 6, 16) # Curve nodes: one per instrument maturity plus an anchor at the effective date. # Initial discount factors are 1.0 everywhere -- the Solver will calibrate them. nodes = { effective: 1.0, datetime.date(2025, 9, 16): 1.0, # 3M datetime.date(2025, 12, 16): 1.0, # 6M datetime.date(2026, 6, 16): 1.0, # 1Y datetime.date(2027, 6, 16): 1.0, # 2Y datetime.date(2028, 6, 16): 1.0, # 3Y datetime.date(2030, 6, 16): 1.0, # 5Y datetime.date(2032, 6, 16): 1.0, # 7Y datetime.date(2035, 6, 16): 1.0, # 10Y } ``` Build the Curve [#build-the-curve] Create a DiscountCurve with [log-linear](../conventions#interpolation-methods) interpolation. This is the standard choice for discount factor curves because it preserves positivity and produces smooth forward rates. ```python continuation curve = DiscountCurve( nodes, interpolation="log_linear", convention="act360", id="sofr", ) ``` Define Instruments [#define-instruments] Create FRA and IRS instruments matching the market quotes. Each IRS `fixed_rate` is in percentage form (4.0 means 4.0%). We use the 5-tuple format `(instrument, target_rate, curve_index, label, currency)` so that the Solver can produce labeled risk output later. Note that IRS targets are in percentage form (matching `IRS.rate()`) while FRA targets are in decimal form (matching `FRA.rate()`). ```python continuation fra_3m = FRA( effective=effective, termination="3M", fixed_rate=4.25, convention="act360", ) fra_6m = FRA( effective=effective, termination="6M", fixed_rate=4.15, convention="act360", ) irs_1y = IRS( effective=effective, termination="1Y", frequency="a", fixed_rate=4.00, convention="act360", float_convention="act360", ) irs_2y = IRS( effective=effective, termination="2Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360", ) irs_3y = IRS( effective=effective, termination="3Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360", ) irs_5y = IRS( effective=effective, termination="5Y", frequency="a", fixed_rate=3.70, convention="act360", float_convention="act360", ) irs_7y = IRS( effective=effective, termination="7Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360", ) irs_10y = IRS( effective=effective, termination="10Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360", ) # 5-tuple format: (instrument, target_rate, curve_index, label, currency) # FRA targets use decimal form (0.0425); IRS targets use percentage form (4.00) instruments = [ (fra_3m, 0.0425, 0, "3M_FRA", "USD"), (fra_6m, 0.0415, 0, "6M_FRA", "USD"), (irs_1y, 4.00, 0, "1Y_IRS", "USD"), (irs_2y, 3.85, 0, "2Y_IRS", "USD"), (irs_3y, 3.75, 0, "3Y_IRS", "USD"), (irs_5y, 3.70, 0, "5Y_IRS", "USD"), (irs_7y, 3.75, 0, "7Y_IRS", "USD"), (irs_10y, 3.85, 0, "10Y_IRS", "USD"), ] ``` Calibrate [#calibrate] Pass the curve and instruments to the Solver and calibrate. The Solver adjusts the discount factors at each node until every instrument reprices to its market quote. ```python continuation solver = Solver(curves=[curve], instruments=instruments) result = solver.iterate() assert result.converged # True ``` Query a few points on the calibrated curve to verify the discount factors and zero rates look reasonable. ```python continuation calibrated = solver.get_curve(0) # Discount factors decrease with maturity df_1y = float(calibrated.discount_factor(datetime.date(2026, 6, 16))) assert 0.94 < df_1y < 0.98 # ~0.961 # Zero rates in decimal form zr_1y = float(calibrated.zero_rate(effective, datetime.date(2026, 6, 16))) assert 0.03 < zr_1y < 0.05 # ~0.039 # Forward rate between 1Y and 2Y fwd_1y_2y = float(calibrated.forward_rate( datetime.date(2026, 6, 16), datetime.date(2027, 6, 16) )) assert 0.03 < fwd_1y_2y < 0.05 # ~0.036 ``` The calibrated curve now prices all input instruments back to their market quotes. You can verify this by checking that the [SolverResult](../api/rates/solver#solverresult) residuals are near zero: ```python continuation assert max(abs(r) for r in result.residuals) < 1e-8 ``` Price Instruments [#price-instruments] Price an off-market 5Y IRS at 3.80% (10bp above the market quote of 3.70%) against the calibrated curve. ```python continuation test_irs = IRS( effective=effective, termination="5Y", frequency="a", fixed_rate=3.80, convention="act360", float_convention="act360", ) # Par rate implied by the calibrated curve par_rate = float(test_irs.rate(calibrated)) assert 3.5 < par_rate < 4.0 # ~3.70 # NPV: non-zero because the fixed rate is off-market npv = float(test_irs.npv(calibrated)) assert npv != 0.0 # off-market instrument has non-zero NPV # Cashflow schedule as a Polars DataFrame cf = test_irs.cashflows(calibrated) assert cf.shape[0] > 0 # at least one cashflow row assert cf.shape[1] > 0 # multiple columns ``` Compute Risk [#compute-risk] Delta and gamma sensitivities show how the instrument's value changes with respect to each calibrating instrument. These are computed from the calibrated solver using [automatic differentiation](../getting-started/type-system) -- to understand why some methods return `Dual` instead of `float`, see the [Type System](../getting-started/type-system.md) guide. ```python continuation delta_df = solver.delta(test_irs, result) assert delta_df.shape[0] == 8 # one row per calibrating instrument assert delta_df.columns == ["instrument_label", "tenor", "sensitivity", "currency"] ``` Gamma captures second-order (convexity) risk: ```python continuation gamma_df = solver.gamma(test_irs, result) assert gamma_df.shape[0] == 8 # 8x8 matrix assert "label" in gamma_df.columns ``` Next Steps [#next-steps] * [Curve Building](rates/curve-building) -- explore different interpolation methods and curve types * [Calibration](calibration) -- multi-curve frameworks and solver algorithm comparison # Serialization (/docs/guides/serialization) All major curve types and the [Solver](../api/rates/solver#solver) support JSON serialization via `to_json()` and `from_json()`. This enables persisting calibrated curves to disk, transmitting them over a network, or inspecting their structure for debugging. Links to [DiscountCurve](../api/rates/curves#discountcurve) for the primary example. *** Discount Curve Round-Trip [#discount-curve-round-trip] The serialization pattern is the same for all curve types: call `to_json()` to get a JSON string, and the corresponding static `from_json()` to restore the object. The restored curve produces identical query results. ```python import datetime import json from vade import DiscountCurve, LineCurve, NelsonSiegel, Solver, Deposit, IRS effective = datetime.date(2025, 6, 16) curve = DiscountCurve( { effective: 1.0, datetime.date(2025, 12, 16): 0.98, datetime.date(2026, 6, 16): 0.96, datetime.date(2027, 6, 16): 0.925, datetime.date(2030, 6, 16): 0.87, }, interpolation="log_linear", convention="act360", ) json_str = curve.to_json() isinstance(json_str, str) # True restored = DiscountCurve.from_json(json_str) float(restored.discount_factor(datetime.date(2026, 6, 16))) # 0.96 assert float(restored.discount_factor(datetime.date(2026, 6, 16))) == float( curve.discount_factor(datetime.date(2026, 6, 16)) ) ``` LineCurve and Parametric Curves [#linecurve-and-parametric-curves] The same `to_json()` / `from_json()` pattern applies to [LineCurve](../api/rates/curves#linecurve) and parametric curve types like [NelsonSiegel](../api/rates/curves#nelsonsiegel). Each type's `from_json()` is a static method that returns the correct type. ```python continuation line = LineCurve( { effective: 0.040, datetime.date(2026, 6, 16): 0.039, datetime.date(2027, 6, 16): 0.038, }, interpolation="linear", convention="act360", ) line_restored = LineCurve.from_json(line.to_json()) float(line_restored.discount_factor(datetime.date(2026, 6, 16))) # 0.9612299019102728 assert float(line_restored.discount_factor(datetime.date(2026, 6, 16))) == float( line.discount_factor(datetime.date(2026, 6, 16)) ) ns = NelsonSiegel( beta0=0.04, beta1=-0.02, beta2=0.03, tau=1.5, base_date=effective, convention="act365f", ) ns_restored = NelsonSiegel.from_json(ns.to_json()) float(ns_restored.zero_rate(effective, datetime.date(2026, 6, 16))) # 0.03189622964353336 assert float(ns_restored.zero_rate(effective, datetime.date(2026, 6, 16))) == float( ns.zero_rate(effective, datetime.date(2026, 6, 16)) ) ``` Solver State [#solver-state] Persisting a calibrated [Solver](../api/rates/solver#solver) captures the full calibration state -- curves, instruments, and weights. This is useful for reproducibility: save after calibration, restore later without re-running the optimization. ```python continuation cal_curve = DiscountCurve( { effective: 1.0, datetime.date(2025, 12, 16): 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, }, interpolation="log_linear", convention="act360", id="sofr", ) dep = Deposit(effective=effective, termination="6m", rate=0.0, convention="act360") irs_1y = IRS( effective=effective, termination="1y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360", ) irs_2y = IRS( effective=effective, termination="2y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360", ) solver = Solver( curves=[cal_curve], instruments=[(dep, 0.0430), (irs_1y, 4.00), (irs_2y, 3.85)], ) result = solver.iterate() assert result.converged df_before = float(solver.get_curve(0).discount_factor(datetime.date(2026, 6, 16))) solver_json = solver.to_json() restored_solver = Solver.from_json(solver_json) df_after = float(restored_solver.get_curve(0).discount_factor(datetime.date(2026, 6, 16))) df_before # 0.9610212072035146 df_after # 0.9610212072035146 assert abs(df_before - df_after) < 1e-10 ``` Inspecting JSON Structure [#inspecting-json-structure] Use `json.loads()` on the serialized string to inspect the internal structure. This is useful for debugging and integrating with external tools that consume JSON. ```python continuation data = json.loads(curve.to_json()) sorted(data.keys()) # ['calendar', 'convention', 'id', 'interpolator', 'modifier', 'nodes'] ``` Next Steps [#next-steps] * [Bootstrap & Parametric Curves](rates/bootstrap-parametric) -- Iterative bootstrap and parametric curve models * [Bonds](credit/bonds) -- Bond analytics and spreads MarketContext Serialization [#marketcontext-serialization] For serializing and restoring a complete market environment (curves, fixings, FX rates, calendars), see the [Market Context guide](market-context#serialization-round-trip). # Architecture (/docs/getting-started/architecture) Vade is a Rust-core Python library for interest rate analytics. All numerical computation -- curve interpolation, AD propagation, solver calibration, cashflow generation -- happens in compiled Rust code. Python provides a thin ergonomic layer of type stubs and re-exports so that the library feels native to Python users while delivering compiled-code performance. The Max-Rust Philosophy [#the-max-rust-philosophy] Vade follows a "max-Rust" architecture: **every computation happens in Rust**. The Python layer contains no business logic at all -- it consists entirely of type stub files (`.pyi`) for IDE support and `__init__.py` modules that re-export Rust objects. This design has three consequences: 1. **Performance.** Curve interpolation, root-finding, AD propagation, and solver iterations are compiled native code with no Python interpreter overhead in the inner loops. 2. **Correctness.** Rust's type system catches entire categories of bugs (null references, data races, type mismatches) at compile time. The numerical core cannot segfault or produce undefined behavior. 3. **AD propagation.** The `Dual` and `Dual2` automatic differentiation types are Rust structs. Every arithmetic operation on them -- including interpolation, log/exp, and spline evaluation -- propagates derivative information through compiled code, making AD nearly free compared to tape-based Python AD frameworks. Layer Stack [#layer-stack] Vade has three layers. Every user request passes through all three: ```text +--------------------------------------------+ | Python API Layer | | | | python/vade/ | | __init__.py re-exports | | autodiff/__init__.pyi type stubs | | marketcurves/__init__.pyi type stubs | | ... | +--------------------------------------------+ | PyO3 Binding Layer | | | | rust/*/py.rs | | #[pyclass] wrappers | | #[pymethods] implementations | | Python dict -> Rust HashMap conversion | | str -> enum parsing | +--------------------------------------------+ | Rust Core | | | | rust/*/mod.rs | | Pure Rust types and algorithms | | No Python dependency | | All business logic lives here | +--------------------------------------------+ ``` **Python API layer** (`python/vade/`). Contains `.pyi` stub files that give IDEs full type information (autocomplete, hover docs, type checking) and `__init__.py` files that re-export compiled classes. There is no `.py` file with business logic anywhere in the codebase. **PyO3 binding layer** (`rust/*/py.rs`). Each Rust module has a `py.rs` file that defines `#[pyclass]` wrappers and `#[pymethods]` implementations. This layer handles the impedance mismatch between Python and Rust: converting Python dicts to Rust `HashMap`s, parsing string arguments into enum variants, and wrapping Rust return values in Python-compatible objects. **Rust core** (`rust/*/mod.rs`). Pure Rust implementations of all algorithms. These files have no PyO3 dependency and can be tested independently of Python. All interpolation, AD arithmetic, schedule generation, solver logic, and cashflow computation lives here. Module Organization [#module-organization] The Rust codebase is organized into eight modules, each following the same `mod.rs` + `py.rs` pattern: | Module | Description | | -------------- | ------------------------------------------------------------------------------------------------------------------- | | `autodiff` | Forward-mode AD types (`Dual`, `Dual2`) and arithmetic operations | | `calendar` | Business day calendars, schedule generation, day count fractions | | `cashflows` | Fixed and floating leg generation, period-level cashflow detail | | `marketcurves` | Discount curves, line curves, composite curves, interpolation, FX spot rates, cross-rate triangulation, FX forwards | | `instruments` | IRS, FRA, ZCS, OIS, bonds, CDS, XCS, deposits, cap/floor | | `models` | Pricing models (Black-76, Bachelier) | | `numerical` | Root-finding solvers (Brent, Newton-Raphson) | | `solver` | Multi-curve Jacobian-based calibration, bootstrap | Each module registers its Python-visible classes through a `register_module` function called from the top-level `lib.rs`: ```text lib.rs -> autodiff::register_module (Dual, Dual2) -> calendar::register_module (BusinessCalendar, Schedule, dcf, ...) -> marketcurves::py::register_module (DiscountCurve, LineCurve, ...) -> marketcurves::fx::py::register_module (FXPair, FXRates, FXForwards) -> cashflows::py::register_module -> instruments::py::register_module -> solver::py::register_module -> numerical::py::register_module ``` Data Flow [#data-flow] A typical operation -- building a discount curve and querying it -- passes through all three layers: ```text Python: curve = DiscountCurve( {date(2024,1,1): 1.0, date(2025,1,1): 0.96}, interpolation="log_linear", ) | v PyO3 py.rs: Parse Python dict -> BTreeMap Parse "log_linear" -> InterpolationMethod::LogLinear Construct RateCurve with interpolation engine Wrap in #[pyclass] PyDiscountCurve | v Python: curve.discount_factor(date(2024, 7, 1)) | v PyO3 py.rs: Convert Python date -> NaiveDate Call RateCurve::discount_factor(NaiveDate) | v Rust mod.rs: index_left binary search for date position Log-linear interpolation on node DFs Return Number (float, Dual, or Dual2) | v PyO3 py.rs: Convert Number -> Python float / Dual / Dual2 | v Python: df = 0.9802... (or Dual with gradient if AD nodes) ``` The same pattern applies to every operation: instrument pricing, solver calibration, risk computation. Python never performs arithmetic -- it is always delegated to Rust. Build System [#build-system] Vade uses [maturin](https://github.com/PyO3/maturin) as its build backend. Maturin compiles the Rust source into a native extension module (`_vade_rs`) that Python imports as a regular package. **Build for development:** ```bash maturin develop --release ``` This compiles the Rust code with optimizations and installs the resulting shared library (`.so` on Linux, `.dylib` on macOS, `.pyd` on Windows) into your Python environment. **Key configuration files:** | File | Purpose | | ---------------- | ----------------------------------------------- | | `Cargo.toml` | Rust package definition, dependencies, features | | `pyproject.toml` | Python package metadata, maturin settings | | `rust/lib.rs` | PyO3 module entry point, submodule registration | **Output structure:** ```text vade/ _vade_rs.so <-- compiled Rust extension _vade_rs.pyi <-- top-level type stubs (if present) __init__.py <-- re-exports from _vade_rs submodules autodiff/ __init__.py <-- re-exports Dual, Dual2 __init__.pyi <-- type stubs for IDE support marketcurves/ __init__.py <-- re-exports DiscountCurve, LineCurve, FXRates, ... __init__.pyi <-- type stubs ... ``` Type stubs (`.pyi` files) are essential because PyO3-compiled objects lack runtime docstrings and type annotations. The stubs provide full IDE support: autocomplete, parameter hints, return type information, and hover docs. Performance [#performance] Rust core provides significant speedup over pure Python for curve calibration and pricing. Compiled native code eliminates interpreter overhead for the tight numerical loops in interpolation, root-finding, and AD propagation. **Why Rust is fast for this domain:** * **No GIL contention.** Numerical computation runs in compiled Rust code, not through the Python interpreter. The Global Interpreter Lock does not bottleneck pure-Rust calculations. * **Compiled arithmetic.** Every `Dual * Dual` operation is a compiled function call, not a Python `__mul__` dispatch through the interpreter. This matters enormously for solver iterations that perform millions of AD-tracked multiplications. * **Cache-friendly data layout.** Rust `Vec` is a contiguous array in memory, identical to a C array. Gradient and Hessian storage benefits from CPU cache locality. * **Zero-copy where possible.** PyO3 can share memory between Python `numpy` arrays and Rust `ndarray` slices without copying data for gradient and Hessian extraction. Exact speedup depends on hardware and workload complexity. Curve calibration with a 9-instrument solver typically completes in under 1ms on modern hardware. Type System Overview [#type-system-overview] Vade's automatic differentiation system produces a distinctive type pattern: methods like `discount_factor()`, `zero_rate()`, and `forward_rate()` return `Union[float, Dual, Dual2]` rather than a plain `float`. The return type depends on the types of the curve's node values: * Float nodes produce float outputs (no derivative tracking) * `Dual` nodes produce `Dual` outputs (first-order derivatives) * `Dual2` nodes produce `Dual2` outputs (first and second-order derivatives) This is a direct consequence of the Rust AD implementation flowing through PyO3 -- the same compiled code path handles all three numeric types via Rust's `Number` enum. See [Type System](type-system) for a full explanation of automatic differentiation, dual numbers, and how to work with union return types. Dependencies [#dependencies] Rust Dependencies [#rust-dependencies] | Crate | Version | Purpose | | ------------ | ------- | --------------------------------------------- | | `pyo3` | 0.28.2 | Python-Rust bindings (PyO3) | | `ndarray` | 0.17 | N-dimensional arrays for gradient/Hessian | | `chrono` | 0.4 | Date handling (`NaiveDate`) | | `indexmap` | 2.7 | Insertion-ordered maps for node storage | | `numpy` | 0.28 | NumPy array interop via PyO3 | | `num-traits` | 0.2 | Generic numeric traits | | `auto_ops` | 0.3 | Operator overloading macro support | | `itertools` | 0.14 | Iterator combinators | | `statrs` | 0.18 | Statistical distributions (for Black-Scholes) | | `serde` | 1.0 | Serialization framework | | `serde_json` | 1.0 | JSON serialization | Python Dependencies [#python-dependencies] | Package | Version | Purpose | | -------- | ------- | ------------------------------- | | `numpy` | >= 1.21 | Array types for gradient access | | `pandas` | >= 1.4 | DataFrame for cashflow tables | | `polars` | >= 0.20 | DataFrame for risk reports | # Installation (/docs/getting-started/installation) From PyPI [#from-pypi] ```bash pip install vade ``` From Source (Development) [#from-source-development] Requires Rust toolchain (1.70+) and maturin: ```bash git clone https://github.com//vade.git cd vade pip install maturin maturin develop --release ``` Verify Installation [#verify-installation] ```python import vade from vade import DiscountCurve import datetime # Build a simple discount curve curve = DiscountCurve( {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.96}, ) df = curve.discount_factor(datetime.date(2024, 7, 1)) print(round(float(df), 6)) # prints a discount factor ``` If the above runs without errors, vade is correctly installed. # Type System (/docs/getting-started/type-system) Vade uses automatic differentiation (AD) to compute exact derivatives for risk sensitivities like delta and gamma. This page explains when and why you encounter AD types in vade, and how to work with them. For full method signatures, see the [Autodiff API reference](../api/autodiff). What is Automatic Differentiation? [#what-is-automatic-differentiation] Automatic differentiation computes exact derivatives by propagating derivative information through every arithmetic operation. Unlike finite differences (which approximate derivatives by bumping inputs) or symbolic differentiation (which manipulates algebraic expressions), AD gives machine-precision results with minimal overhead. Why this matters for quantitative finance: delta, gamma, and bucket-level risk sensitivities all require derivatives of instrument prices with respect to market inputs (discount factors, rates, spreads). AD computes these derivatives as a natural byproduct of the pricing calculation -- no extra bumps, no approximation error. Vade implements **forward-mode** AD via dual numbers. Two types are provided: `Dual` for first-order derivatives (gradients) and `Dual2` for first and second-order derivatives (gradients and Hessians). Dual Numbers [#dual-numbers] A [Dual](../api/autodiff#dual) number is "a number that remembers what it depends on." It carries a real value alongside a gradient vector tracking how that value changes with respect to named variables. Create a dual number with a variable name and value: ```python d = Dual("x", 3.0) print(d.real) # 3.0 print(d.vars) # ['x'] print(d.gradient()) # [1.] -- derivative of x with respect to x is 1 ``` Arithmetic operations propagate derivatives automatically: ```python continuation result = d * d # x^2 print(result.real) # 9.0 print(result.gradient()) # [6.] -- d/dx(x^2) = 2x = 6.0 ``` Multi-variable expressions track all dependencies: ```python continuation y = Dual("y", 4.0) f = d * y + d ** 2.0 # x*y + x^2 print(f.real) # 21.0 print(f.vars) # ['x', 'y'] print(f.gradient()) # [10. 3.] -- df/dx = y + 2x = 10, df/dy = x = 3 ``` When you need a plain number from a Dual result, use `float()`: ```python continuation plain = float(result) print(type(plain).__name__) # float print(plain) # 9.0 ``` Dual2 Numbers [#dual2-numbers] [Dual2](../api/autodiff#dual2) extends dual numbers to track second-order derivatives. This is needed for gamma risk (the second derivative of price with respect to rates). ```python d2 = Dual2("x", 3.0) result = d2 * d2 # x^2 print(result.real) # 9.0 print(result.gradient()) # [6.] -- first derivative: d/dx(x^2) = 2x = 6 print(result.gradient2()) # [[2.]] -- second derivative: d^2/dx^2(x^2) = 2 ``` Higher-order chain rule propagation works automatically: ```python continuation cubic = d2 ** 3.0 # x^3 print(cubic.real) # 27.0 print(cubic.gradient()) # [27.] -- d/dx(x^3) = 3x^2 = 27 print(cubic.gradient2()) # [[18.]] -- d^2/dx^2(x^3) = 6x = 18 ``` Dual2 is more expensive than Dual (it stores an n-by-n half-Hessian matrix in addition to the n-element gradient), so it is only used when second-order risk is needed. Union Return Types [#union-return-types] This is the most distinctive part of vade's type system. Curve methods like `discount_factor()`, `zero_rate()`, and `forward_rate()` return `Union[float, Dual, Dual2]` rather than a plain `float`. The return type depends on the types of the curve's node values. **Float nodes produce float outputs** -- no derivative tracking: ```python import datetime curve_f = DiscountCurve( {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.96}, interpolation="log_linear", ) df = curve_f.discount_factor(datetime.date(2024, 7, 1)) print(type(df).__name__) # float ``` **Dual nodes produce Dual outputs** -- first-order derivatives available: ```python continuation curve_d = DiscountCurve( {datetime.date(2024, 1, 1): Dual("v0", 1.0), datetime.date(2025, 1, 1): Dual("v1", 0.96)}, interpolation="log_linear", ) df_d = curve_d.discount_factor(datetime.date(2024, 7, 1)) print(type(df_d).__name__) # Dual print(round(df_d.real, 6)) # same numeric value as float case print(df_d.vars) # ['v1', 'v0'] -- tracks both node variables ``` **Dual2 nodes produce Dual2 outputs** -- first and second-order derivatives: ```python continuation curve_d2 = DiscountCurve( {datetime.date(2024, 1, 1): Dual2("v0", 1.0), datetime.date(2025, 1, 1): Dual2("v1", 0.96)}, interpolation="log_linear", ) df_d2 = curve_d2.discount_factor(datetime.date(2024, 7, 1)) print(type(df_d2).__name__) # Dual2 print(len(df_d2.gradient())) # 2 -- gradient w.r.t. both node variables ``` The pattern is consistent across all curve types: [DiscountCurve](../api/rates/curves#discountcurve), [LineCurve](../api/rates/curves#linecurve), and composite curves all follow the same rule. The input node type determines the output type. AD in Practice [#ad-in-practice] You rarely create Dual-valued curves manually. The [Solver](../api/rates/solver#solver) does this automatically during calibration: it replaces your initial float node values with Dual (or Dual2) numbers, solves for the correct values, and returns a calibrated curve whose nodes carry derivative information. ```bash # After solver.iterate(), calibrated curve has Dual-valued nodes. # Any pricing operation on this curve returns Dual results: npv = instrument.npv(curves) # npv is a Dual npv.gradient() # derivatives w.r.t. each curve node # The Solver.delta() method extracts these into a structured report: delta_df = solver.delta() # Polars DataFrame with risk by instrument ``` See the [Risk guide](../guides/rates/risk) for a complete worked example of delta and gamma computation from a calibrated solver. When you need a plain number from any AD result, wrap with `float()`: ```bash rate = float(instrument.rate(curves)) # strips AD, returns plain float ``` Summary [#summary] | Type | Tracks | Use Case | Created By | | ------- | ---------------------------- | ------------------ | ----------------------- | | `float` | Nothing | Plain computation | Default node values | | `Dual` | First derivatives (gradient) | Delta risk | Solver calibration | | `Dual2` | First + second derivatives | Delta + gamma risk | Solver with Dual2 nodes | The key insight: **you choose the level of derivative tracking by choosing the node value type.** Float nodes give you fast, derivative-free pricing. Dual nodes give you delta. Dual2 nodes give you delta and gamma. The same compiled Rust code handles all three -- no separate "bumped" pricing pass is needed. # FX (/docs/api/fx/fx-rates) FX rates with graph-based cross-rate triangulation, currency conversion, and FX forward pricing. See also: [FX Rates & Forwards Guide](../guides/fx/fx-rates-forwards) | [Cross-Currency Swaps Guide](../guides/fx/cross-currency.md) | [Non-Deliverable Instruments Guide](../guides/fx/non-deliverable.md) All types are available via flat import: ```bash from vade import FXPair, FXRates, FXForwards ``` *** FXPair [#fxpair] Currency pair representation. *Rust-backed.* Constructor [#constructor] ```bash FXPair(pair_str) ``` Parameters [#parameters] | Name | Type | Default | Description | | ---------- | ----- | ---------- | -------------------------------------------------------- | | `pair_str` | `str` | *required* | Six-character lowercase currency pair (e.g., `"eurusd"`) | Properties [#properties] | Property | Type | Description | | -------- | ----- | ------------------------------------------- | | `.lhs` | `str` | Left-hand (base) currency (e.g., `"eur"`) | | `.rhs` | `str` | Right-hand (quote) currency (e.g., `"usd"`) | Methods [#methods] | Method | Returns | Description | | ----------- | -------- | ----------------------------------- | | `.invert()` | `FXPair` | Return pair with currencies swapped | Example [#example] ```python from vade import FXPair pair = FXPair("eurusd") pair.lhs # 'eur' pair.rhs # 'usd' inv = pair.invert() inv.lhs # 'usd' inv.rhs # 'eur' ``` Advanced Example [#advanced-example] ```python from vade import FXPair # Parse multiple currency pairs eurusd = FXPair("eurusd") gbpusd = FXPair("gbpusd") usdjpy = FXPair("usdjpy") # Access base (lhs) and quote (rhs) currencies assert eurusd.lhs == "eur" assert eurusd.rhs == "usd" assert gbpusd.lhs == "gbp" assert gbpusd.rhs == "usd" assert usdjpy.lhs == "usd" assert usdjpy.rhs == "jpy" # Inversion swaps base and quote inv = eurusd.invert() assert inv.lhs == "usd" assert inv.rhs == "eur" # Double inversion returns to original inv2 = inv.invert() assert inv2.lhs == "eur" assert inv2.rhs == "usd" # Cross-rate pair eurgbp = FXPair("eurgbp") assert eurgbp.lhs == "eur" assert eurgbp.rhs == "gbp" # Invert cross-rate gbpeur = eurgbp.invert() assert gbpeur.lhs == "gbp" assert gbpeur.rhs == "eur" ``` *** FXRates [#fxrates] FX rates with graph-based cross-rate triangulation and automatic AD variables. *Rust-backed.* Builds a currency graph from input rates and triangulates any cross-rate not directly provided. Constructor [#constructor-1] ```bash FXRates(fx_rates, settlement) ``` Parameters [#parameters-1] | Name | Type | Default | Description | | ------------ | ------------------ | ---------- | ------------------------------------------------------- | | `fx_rates` | `Dict[str, float]` | *required* | Currency pair rates as `{"eurusd": 1.1, "gbpusd": 1.3}` | | `settlement` | `datetime.date` | *required* | Settlement date for spot rates | Properties [#properties-1] | Property | Type | Description | | ------------- | --------------- | ------------------------------------- | | `.currencies` | `list[str]` | List of currencies in the rate system | | `.settlement` | `datetime.date` | Settlement date | Methods [#methods-1] | Method | Returns | Description | | ------------------------------------- | --------------- | ------------------------------------------------ | | `.rate(pair)` | `float \| Dual` | Rate for pair (triangulates if needed) | | `.convert(value, domestic, foreign)` | `float \| Dual` | Convert amount from domestic to foreign currency | | `.convert_positions(positions, base)` | `float \| Dual` | Convert position dict to base currency total | | `.restate(new_pairs)` | `FXRates` | Restate rates in terms of new pair definitions | FX methods may return [Dual](../getting-started/type-system#dual) values when curves have AD-enabled nodes. Example [#example-1] ```python import datetime from vade import FXRates fx = FXRates({"eurusd": 1.10, "gbpusd": 1.30}, datetime.date(2024, 1, 3)) # Direct rate lookup eurusd = fx.rate("eurusd") assert 1.09 < float(eurusd) < 1.11 # Cross-rate triangulation: EUR/GBP derived from EUR/USD and GBP/USD eurgbp = fx.rate("eurgbp") assert 0.8 < float(eurgbp) < 0.9 # ~1.10/1.30 = 0.846 # Currency conversion usd_amount = fx.convert(1_000_000.0, "eur", "usd") assert float(usd_amount) > 1_000_000 # EUR to USD at 1.10 fx.currencies # ['eur', 'gbp', 'usd'] fx.settlement # datetime.date(2024, 1, 3) ``` *** FXForwards [#fxforwards] FX forwards via interest rate parity. *Rust-backed.* Computes forward FX rates and forward points using spot rates and discount curves for each currency. Constructor [#constructor-2] ```bash FXForwards(fx_rates, settlements) ``` Parameters [#parameters-2] | Name | Type | Default | Description | | ------------- | -------------------------- | ---------- | ---------------------------------- | | `fx_rates` | `FXRates` | *required* | Spot FX rates | | `settlements` | `Dict[str, datetime.date]` | *required* | Settlement dates per currency code | Methods [#methods-2] | Method | Returns | Description | | --------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `.rate(pair, delivery, curves)` | `float \| Dual` | Forward FX rate at delivery date. `curves` is `Dict[str, DiscountCurve]` mapping currency codes to [discount curves](curves#discountcurve). | | `.points(pair, delivery, curves)` | `float \| Dual` | Forward points (forward rate minus spot rate) | Return types are [Dual](autodiff#dual) when AD variables are tracked. Example [#example-2] ```python import datetime from vade import DiscountCurve, FXRates, FXForwards # Spot rates fx = FXRates({"eurusd": 1.10}, datetime.date(2024, 1, 3)) # Discount curves per currency usd_nodes = { datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.96, } eur_nodes = { datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97, } usd_curve = DiscountCurve(usd_nodes, convention="act365f", id="usd") eur_curve = DiscountCurve(eur_nodes, convention="act365f", id="eur") # FX forwards settlements = {"usd": datetime.date(2024, 1, 3), "eur": datetime.date(2024, 1, 3)} fwd = FXForwards(fx, settlements) curves = {"usd": usd_curve, "eur": eur_curve} delivery = datetime.date(2025, 1, 3) fwd_rate = fwd.rate("eurusd", delivery, curves) assert float(fwd_rate) > 1.0 # forward rate reflects interest rate differential fwd_pts = fwd.points("eurusd", delivery, curves) assert isinstance(float(fwd_pts), float) # forward points (forward - spot) ``` Advanced Example [#advanced-example-1] ```python import datetime from vade import DiscountCurve, FXRates, FXForwards # Spot rates for two major pairs spot_date = datetime.date(2024, 6, 3) fx = FXRates({"eurusd": 1.08, "gbpusd": 1.27}, spot_date) # Discount curves per currency (USD, EUR, GBP) # Higher discount factor = lower interest rate usd_nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 7, 1): 0.955} eur_nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 7, 1): 0.965} gbp_nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 7, 1): 0.960} usd_curve = DiscountCurve(usd_nodes, convention="act365f", interpolation="log_linear", id="usd") eur_curve = DiscountCurve(eur_nodes, convention="act365f", interpolation="log_linear", id="eur") gbp_curve = DiscountCurve(gbp_nodes, convention="act365f", interpolation="log_linear", id="gbp") # Settlement dates per currency settlements = { "usd": spot_date, "eur": spot_date, "gbp": spot_date, } fwd = FXForwards(fx, settlements) curves = {"usd": usd_curve, "eur": eur_curve, "gbp": gbp_curve} # Forward rates at multiple delivery dates show term structure delivery_1m = datetime.date(2024, 7, 3) delivery_3m = datetime.date(2024, 9, 3) delivery_6m = datetime.date(2024, 12, 3) delivery_1y = datetime.date(2025, 6, 3) rate_1m = float(fwd.rate("eurusd", delivery_1m, curves)) rate_3m = float(fwd.rate("eurusd", delivery_3m, curves)) rate_6m = float(fwd.rate("eurusd", delivery_6m, curves)) rate_1y = float(fwd.rate("eurusd", delivery_1y, curves)) # Forward rates increase with tenor (EUR rates < USD rates => EUR appreciates) assert 1.08 < rate_1m < rate_3m < rate_6m < rate_1y # Forward points = forward rate - spot rate pts_1m = float(fwd.points("eurusd", delivery_1m, curves)) pts_3m = float(fwd.points("eurusd", delivery_3m, curves)) pts_6m = float(fwd.points("eurusd", delivery_6m, curves)) pts_1y = float(fwd.points("eurusd", delivery_1y, curves)) # Points are positive and increase with tenor assert 0 < pts_1m < pts_3m < pts_6m < pts_1y assert 0.001 < pts_1m < 0.01 # small for 1M assert 0.005 < pts_1y < 0.02 # larger for 1Y # GBP/USD forward rates via the same curve set gbp_rate_1y = float(fwd.rate("gbpusd", delivery_1y, curves)) assert 1.27 < gbp_rate_1y < 1.30 # GBP also appreciates vs USD # Cross-rate: EUR/GBP derived from triangulation eurgbp_fwd = float(fwd.rate("eurgbp", delivery_1y, curves)) assert 0.84 < eurgbp_fwd < 0.87 # ~1.09/1.28 ``` # FX API Reference (/docs/api/fx) Complete API reference for FX analytics: spot/forward rates, cross-currency instruments, and non-deliverable products. *** Pages [#pages] * [Instruments](./instruments) -- XCS, FXForward, NDF, NDIRS, NDXCS * [FX Rates & Forwards](./fx-rates) -- FXRates, FXForwards, FXPair * [Curves](../rates/curves) -- FXImpliedCurve See Also [#see-also] * [FX Rates & Forwards Guide](../../guides/fx/fx-rates-forwards) -- FX rate triangulation and forward construction * [Cross-Currency Swaps Guide](../../guides/fx/cross-currency) -- Cross-currency swap pricing and risk * [Non-Deliverable Instruments Guide](../../guides/fx/non-deliverable) -- NDF and non-deliverable swap pricing for restricted currencies * [Context](../context) -- MarketContext with fx\_rates for cross-currency pricing * [Solver](../rates/solver) -- Multi-curve calibration with FX rate support # FX Instruments (/docs/api/fx/instruments) Cross-currency swaps, FX forwards, and non-deliverable instruments. All types are available via flat import: ```bash from vade import XCS, FXForward, NDF, NDIRS, NDXCS ``` Or via product-path import: ```bash from vade.instruments.fx import XCS, FXForward, NDF, NDIRS, NDXCS ``` See [Conventions](../../conventions) for all accepted string parameter values. **Contents:** [XCS](#xcs) | [FXForward](#fxforward) | [NDF](#ndf) | [NDIRS](#ndirs) | [NDXCS](#ndxcs) *** XCS [#xcs] Cross Currency Swap with 4-curve pricing model. *Python wrapper composing legs across currencies.* See also: [Cross-Currency Swaps Guide](../guides/fx/cross-currency) for cross-currency pricing workflows. **Alias:** `XCS = CrossCurrencySwap` Constructor [#constructor] ```bash XCS( *, effective=None, termination=None, frequency="q", leg1_currency="EUR", leg2_currency="USD", leg1_notional=1_000_000.0, leg2_notional=1_100_000.0, initial_fx_rate=1.1, fixed_rate=None, notional_exchange=True, convention="act360", leg2_convention=None, fixing_method="rfr_payment_delay", spread=0.0, modifier="mf", stub="shortfront", mtm_leg=None, ) ``` Parameters [#parameters] | Name | Type | Default | Description | | ------------------- | ------------------------- | --------------------- | --------------------------------------------------------- | | `effective` | `datetime.date` or `None` | `None` | Start date | | `termination` | `date`, `str`, or `None` | `None` | End date or tenor string | | `frequency` | `str` | `"q"` | Payment frequency | | `leg1_currency` | `str` | `"EUR"` | Leg 1 currency code | | `leg2_currency` | `str` | `"USD"` | Leg 2 currency code | | `leg1_notional` | `float` | `1_000_000.0` | Leg 1 notional amount | | `leg2_notional` | `float` | `1_100_000.0` | Leg 2 notional amount | | `initial_fx_rate` | `float` | `1.1` | Initial FX rate (leg2/leg1) | | `fixed_rate` | `float` or `None` | `None` | Fixed rate on one leg (if applicable) | | `notional_exchange` | `bool` | `True` | Whether to exchange notionals at start and end | | `convention` | `str` | `"act360"` | Day count convention for leg 1 | | `leg2_convention` | `str` or `None` | `None` | Day count convention for leg 2 (defaults to `convention`) | | `fixing_method` | `str` | `"rfr_payment_delay"` | Rate fixing method | | `spread` | `float` | `0.0` | Spread on leg 2 (basis points) | | `modifier` | `str` | `"mf"` | Business day adjustment rule | | `stub` | `str` | `"shortfront"` | Stub period preference | | `mtm_leg` | `int` or `None` | `None` | Mark-to-market reset leg (`1` or `2`) | See [Conventions](../conventions) for accepted values for `frequency`, `convention`, `fixing_method`, `modifier`, and `stub`. Methods [#methods] XCS methods require four separate curves (discount and forecast for each currency) plus [FXRates](fx#fxrates). | Method | Returns | Description | | ------------------------------------------------------------------------------------ | ----------- | --------------------------- | | `.rate(leg1_disc, leg1_forecast, leg2_disc, leg2_forecast, fx_rates, metric="leg2")` | `float` | Cross-currency basis spread | | `.npv(leg1_disc, leg1_forecast, leg2_disc, leg2_forecast, fx_rates)` | `float` | Net present value | | `.cashflows(leg1_disc, leg1_forecast, leg2_disc, leg2_forecast)` | `DataFrame` | Period-level cashflow table | Example [#example] ```python import datetime from vade import XCS, DiscountCurve, FXRates eur_disc = DiscountCurve( {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97, datetime.date(2026, 1, 1): 0.94}, interpolation="log_linear", convention="act365f", id="eur_disc", ) eur_fcast = DiscountCurve( {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97, datetime.date(2026, 1, 1): 0.94}, interpolation="log_linear", convention="act365f", id="eur_fcast", ) usd_disc = DiscountCurve( {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.96, datetime.date(2026, 1, 1): 0.92}, interpolation="log_linear", convention="act365f", id="usd_disc", ) usd_fcast = DiscountCurve( {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.96, datetime.date(2026, 1, 1): 0.92}, interpolation="log_linear", convention="act365f", id="usd_fcast", ) fx = FXRates({"eurusd": 1.10}, settlement=datetime.date(2024, 1, 1)) xcs = XCS( effective=datetime.date(2024, 1, 1), termination="2Y", frequency="q", leg1_currency="EUR", leg2_currency="USD", leg1_notional=1_000_000.0, leg2_notional=1_100_000.0, initial_fx_rate=1.10, convention="act360", ) float(xcs.rate(eur_disc, eur_fcast, usd_disc, usd_fcast, fx)) # cross-currency basis spread float(xcs.npv(eur_disc, eur_fcast, usd_disc, usd_fcast, fx)) # NPV in base currency ``` *** FXForward [#fxforward] FX Forward instrument. *Rust-backed.* See also: [FX Rates & Forwards Guide](../guides/fx/fx-rates-forwards) for FX forward pricing. Constructor [#constructor-1] ```bash FXForward( *, pair="eurusd", notional=1_000_000.0, delivery=None, currency="USD", ) ``` Parameters [#parameters-1] | Name | Type | Default | Description | | ---------- | ------------------------- | ------------- | -------------------------------------------- | | `pair` | `str` | `"eurusd"` | Currency pair (e.g., `"eurusd"`, `"gbpusd"`) | | `notional` | `float` | `1_000_000.0` | Notional amount | | `delivery` | `datetime.date` or `None` | `None` | Delivery date | | `currency` | `str` | `"USD"` | Settlement currency | Methods [#methods-1] FXForward methods take a discount curve, forecast curve, and [FXRates](fx#fxrates). | Method | Returns | Description | | -------------------------------------------------- | ----------- | --------------------------- | | `.rate(disc_curve, forecast_curve, fx_rates)` | `float` | Forward FX rate | | `.npv(disc_curve, forecast_curve, fx_rates)` | `float` | Net present value | | `.cashflows(disc_curve, forecast_curve, fx_rates)` | `DataFrame` | Period-level cashflow table | Example [#example-1] ```python import datetime from vade import FXForward, DiscountCurve, FXRates eur_curve = DiscountCurve( {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97}, interpolation="log_linear", convention="act365f", id="eur_curve", ) usd_curve = DiscountCurve( {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.96}, interpolation="log_linear", convention="act365f", id="usd_curve", ) fx = FXRates({"eurusd": 1.10}, settlement=datetime.date(2024, 1, 1)) fxf = FXForward( pair="eurusd", notional=1_000_000.0, delivery=datetime.date(2024, 7, 1), currency="USD", ) float(fxf.rate(usd_curve, eur_curve, fx)) # forward FX rate float(fxf.npv(usd_curve, eur_curve, fx)) # NPV of the forward position ``` *** Non-Deliverable Instruments [#non-deliverable-instruments] Non-deliverable (ND) instruments settle in a convertible currency rather than the restricted currency in which cashflows accrue. FX conversion uses per-period fixings (known or IRP-implied). *** NDF [#ndf] Non-Deliverable Forward: single-currency net settlement of an FX forward. Subclass of `FXForward`. See also: [Non-Deliverable Instruments Guide](../guides/fx/non-deliverable) for NDF, NDIRS, and NDXCS workflows. Constructor [#constructor-2] ```bash NDF( *, spec=None, # Market convention ("usdbrl_ndf", "usdkrw_ndf", ...) effective=None, # Effective / trade date delivery=None, # Settlement date pair="usdbrl", # FX pair (restricted vs settlement) notional=1_000_000.0, currency="USD", # Notional currency settlement_currency="usd", contracted_rate=None, # Agreed FX rate (None = at-market) fx_fixings=None, # dict[date, float] for known fixings ) ``` Methods [#methods-2] | Method | Signature | Returns | | ------------- | ---------------------------------------- | ----------------------------------------- | | `rate` | `(disc_curve, forecast_curve, fx_rates)` | Implied forward rate (IRP) | | `npv` | `(disc_curve, forecast_curve, fx_rates)` | Net settlement NPV in settlement currency | | `cashflows` | `(disc_curve, forecast_curve, fx_rates)` | Polars DataFrame | | `spread` | `(disc_curve, forecast_curve, fx_rates)` | Spread to contracted rate | | `pillar_date` | `()` | Delivery date | | `to_json` | `()` | JSON-serializable dict | | `from_json` | `(data)` | Reconstruct NDF from dict | Example [#example-2] ```python import datetime from vade import NDF, DiscountCurve, FXRates ndf = NDF( spec="usdbrl_ndf", effective=datetime.date(2025, 6, 16), delivery=datetime.date(2025, 9, 16), notional=1_000_000.0, contracted_rate=5.25, ) usd = DiscountCurve( {datetime.date(2025, 6, 16): 1.0, datetime.date(2026, 6, 16): 0.96}, interpolation="log_linear", convention="act360", ) brl = DiscountCurve( {datetime.date(2025, 6, 16): 1.0, datetime.date(2026, 6, 16): 0.88}, interpolation="log_linear", convention="act360", ) fxr = FXRates({"usdbrl": 5.20}, settlement=datetime.date(2025, 6, 16)) fair_rate = ndf.rate(usd, brl, fxr) npv = ndf.npv(usd, brl, fxr) assert abs(float(fair_rate)) < 10 assert abs(float(npv)) < 1e8 ``` *** NDIRS [#ndirs] Non-Deliverable Interest Rate Swap: IRS where cashflows accrue in a restricted currency but settle in a convertible currency via per-period FX conversion. Subclass of `InterestRateSwap`. Uses a 3-curve model: settlement discount, restricted forecast, restricted discount. Constructor [#constructor-3] ```bash NDIRS( *, spec=None, # Market convention ("usdbrl_ndirs", ...) effective=None, termination=None, # date or tenor string ("5Y") frequency="s", fixed_rate=0.0, notional=1_000_000.0, convention="act360", settlement_currency="usd", pair="usdbrl", fx_fixings=None, # dict[date, float] known FX fixings ) ``` Methods [#methods-3] | Method | Signature | Returns | | ------------- | ---------------------------------------------------- | ---------------------------------------- | | `rate` | `(curves=[disc, forecast, leg2_disc], fx_rates=fxr)` | Par fixed rate | | `npv` | `(curves=[disc, forecast, leg2_disc], fx_rates=fxr)` | NPV in settlement currency | | `cashflows` | `(curves=[disc, forecast, leg2_disc], fx_rates=fxr)` | Polars DataFrame with settlement columns | | `spread` | `(curves=[disc, forecast, leg2_disc], fx_rates=fxr)` | Par spread on float leg | | `pillar_date` | `()` | Maturity date | | `to_json` | `()` | JSON-serializable dict | | `from_json` | `(data)` | Reconstruct NDIRS from dict | Curve arguments: `disc_curve` = settlement discount, `forecast_curve` = restricted forecast, `leg2_disc_curve` = restricted discount (for IRP forwards). Example [#example-3] ```python import datetime from vade import NDIRS, DiscountCurve, FXRates ndirs = NDIRS( spec="usdbrl_ndirs", effective=datetime.date(2025, 6, 16), termination="2Y", fixed_rate=10.0, notional=1_000_000.0, ) usd = DiscountCurve( {datetime.date(2025, 6, 16): 1.0, datetime.date(2028, 6, 16): 0.89}, interpolation="log_linear", convention="act360", ) brl = DiscountCurve( {datetime.date(2025, 6, 16): 1.0, datetime.date(2028, 6, 16): 0.71}, interpolation="log_linear", convention="act360", ) fxr = FXRates({"usdbrl": 5.20}, settlement=datetime.date(2025, 6, 16)) par_rate = ndirs.rate(curves=[usd, brl, brl], fx_rates=fxr) npv = ndirs.npv(curves=[usd, brl, brl], fx_rates=fxr) assert 0 < float(par_rate) < 30 assert abs(float(npv)) < 1e8 ``` *** NDXCS [#ndxcs] Non-Deliverable Cross-Currency Swap: one leg is non-deliverable with cashflows converted via per-period FX fixings and settled in a convertible currency. Supports notional exchanges and MTM resets on the ND leg. Subclass of `CrossCurrencySwap`. Constructor [#constructor-4] ```bash NDXCS( *, spec=None, # Market convention ("usdbrl_ndxcs", ...) effective=None, termination=None, # date or tenor string ("5Y") frequency="q", leg1_currency="USD", leg2_currency="BRL", leg1_notional=1_000_000.0, leg2_notional=5_000_000.0, initial_fx_rate=5.0, fixed_rate=None, # None = float-float notional_exchange=True, settlement_currency="usd", nd_leg=2, # Which leg is non-deliverable (1 or 2) pair=None, # FX pair (derived from currencies if None) fx_fixings=None, # dict[date, float] for ALL FX fixing needs mtm_leg=None, # Optional MTM notional reset leg ) ``` Methods [#methods-4] | Method | Signature | Returns | | ------------- | ---------------------------------------------------------------- | ---------------------------------------- | | `rate` | `(leg1_disc, leg1_forecast, leg2_disc, leg2_forecast, fx_rates)` | Par basis spread on ND leg | | `npv` | `(leg1_disc, leg1_forecast, leg2_disc, leg2_forecast, fx_rates)` | NPV in settlement currency | | `cashflows` | `(leg1_disc, leg1_forecast, leg2_disc, leg2_forecast, fx_rates)` | Polars DataFrame with settlement columns | | `spread` | `(leg1_disc, leg1_forecast, leg2_disc, leg2_forecast, fx_rates)` | Par floating spread | | `pillar_date` | `()` | Maturity date | | `to_json` | `()` | JSON-serializable dict | | `from_json` | `(data)` | Reconstruct NDXCS from dict | Example [#example-4] ```bash import datetime from vade import NDXCS, DiscountCurve, FXRates ndxcs = NDXCS( effective=datetime.date(2025, 6, 16), termination="2Y", leg1_currency="USD", leg2_currency="BRL", settlement_currency="usd", nd_leg=2, pair="usdbrl", leg1_notional=1_000_000.0, leg2_notional=5_000_000.0, initial_fx_rate=5.0, frequency="q", convention="act360", ) usd = DiscountCurve( {datetime.date(2025, 6, 16): 1.0, datetime.date(2028, 6, 16): 0.89}, interpolation="log_linear", convention="act360", ) brl = DiscountCurve( {datetime.date(2025, 6, 16): 1.0, datetime.date(2028, 6, 16): 0.71}, interpolation="log_linear", convention="act360", ) fxr = FXRates({"usdbrl": 5.20}, settlement=datetime.date(2025, 6, 16)) rate = ndxcs.rate(usd, usd, brl, brl, fxr) npv = ndxcs.npv(usd, usd, brl, brl, fxr) assert abs(float(rate)) < 500 assert abs(float(npv)) < 1e8 ``` *** *** See Also [#see-also] * [FX Guides](../../guides/fx/) -- FX rates, cross-currency, and non-deliverable guides * [FX Rates & Forwards API](fx-rates) -- FXRates, FXForwards, FXPair # Credit API Reference (/docs/api/credit) Complete API reference for credit analytics: bonds, credit derivatives, and spread analysis. *** Pages [#pages] * [Instruments](./instruments) -- FixedRateBond, Bill, FloatRateNote, SubPeriodFRN, ZeroCouponBond, StepUpBond, AmortizingBond, PIKBond, CappedFloatRateNote, AssetSwap, CallableBond, CDS * [Curves](../rates/curves) -- CreditImpliedCurve, FittedBondCurve, SpreadCurve See Also [#see-also] * [Credit Guides](../../guides/credit/) -- Bond analytics, CDS pricing, fitted curves, and spread analysis guides # Credit Instruments (/docs/api/credit/instruments) Bonds, credit derivatives, and structured products for pricing, risk, and spread analytics. All types are available via flat import: ```bash from vade import FixedRateBond, Bill, FloatRateNote, SubPeriodFRN from vade import ZeroCouponBond, StepUpBond, AmortizingBond, PIKBond from vade import CappedFloatRateNote, AssetSwap, CallableBond, CDS ``` Or via product-path import: ```bash from vade.instruments.credit import FixedRateBond, Bill, FloatRateNote, SubPeriodFRN from vade.instruments.credit import ZeroCouponBond, StepUpBond, AmortizingBond, PIKBond from vade.instruments.credit import CappedFloatRateNote, AssetSwap, CallableBond, CDS ``` See [Conventions](../../conventions) for all accepted string parameter values. **Contents:** [FixedRateBond](#fixedratebond) | [Bill](#bill) | [FloatRateNote](#floatratenote) | [SubPeriodFRN](#subperiodfrn) | [ZeroCouponBond](#zerocouponbond) | [StepUpBond](#stepupbond) | [AmortizingBond](#amortizingbond) | [PIKBond](#pikbond) | [CappedFloatRateNote](#cappedfloatratenote) | [AssetSwap](#assetswap) | [CallableBond](#callablebond) | [CDS](#cds) *** FixedRateBond [#fixedratebond] Fixed Rate Bond with full analytics suite. *Python wrapper composing FixedLeg with bond analytics.* See also: [Bonds Guide](../guides/credit/bonds) for bond pricing, yield analysis, and risk metrics. Constructor [#constructor] ```bash FixedRateBond( *, spec=None, effective=None, termination=None, coupon=0.0, frequency="s", settlement_days=1, ex_div_days=0, calendar=None, currency="USD", convention="actactisda", accrued_convention=None, face_value=100.0, modifier="none", disc_curve_id=None, ) ``` Parameters [#parameters] | Name | Type | Default | Description | | -------------------- | ------------------------- | -------------- | ------------------------------------------------------------------ | | `spec` | `str` or `None` | `None` | Instrument spec name | | `effective` | `datetime.date` or `None` | `None` | Issue or settlement date | | `termination` | `date`, `str`, or `None` | `None` | Maturity date or tenor string | | `coupon` | `float` | `0.0` | Coupon rate (percentage, e.g., `5.0` for 5%) | | `frequency` | `str` | `"s"` | Coupon payment frequency | | `settlement_days` | `int` | `1` | Settlement lag in business days | | `ex_div_days` | `int` | `0` | Ex-dividend days before coupon payment | | `calendar` | `str` or `None` | `None` | Named calendar | | `currency` | `str` | `"USD"` | Currency code | | `convention` | `str` | `"actactisda"` | Day count convention for coupon accrual | | `accrued_convention` | `str` or `None` | `None` | Separate day count for accrued interest (defaults to `convention`) | | `face_value` | `float` | `100.0` | Face value of the bond | | `modifier` | `str` | `"none"` | Business day adjustment rule | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | See [Conventions](../conventions) for accepted values for `frequency`, `convention`, and `modifier`. Methods [#methods] Standard Methods [#standard-methods] | Method | Returns | Description | | ------------------------------------ | ----------- | --------------------------- | | `.rate(curves, *, solver=None)` | `float` | Par rate against curves | | `.npv(curves, *, solver=None)` | `float` | Net present value | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | | `.spread(curves, *, solver=None)` | `float` | Par spread | Price and Yield [#price-and-yield] | Method | Returns | Description | | --------------------------------------------------------- | ------- | ----------------------------------- | | `.price(curves, *, solver=None, settlement=None)` | `float` | Clean price | | `.dirty_price(curves, *, solver=None, settlement=None)` | `float` | Dirty price (clean + accrued) | | `.accrued_interest(settlement)` | `float` | Accrued interest at settlement date | | `.ytm(clean_price, settlement, convention="periodic")` | `float` | Yield to maturity from clean price | | `.price_from_ytm(ytm, settlement, convention="periodic")` | `float` | Clean price from yield to maturity | YTM `convention` accepts `"periodic"`, `"annual"`, `"semi_annual"`, or `"continuous"`. Risk Analytics [#risk-analytics] | Method | Returns | Description | | ------------------------------------------------------------------------------------ | ------- | ------------------------------- | | `.duration(curves, *, solver=None, metric="modified", settlement=None)` | `float` | Modified or Macaulay duration | | `.convexity(curves, *, solver=None, settlement=None)` | `float` | Price convexity | | `.dv01(curves, *, solver=None)` | `float` | Dollar value of 1bp | | `.z_spread(curves, *, solver=None, price=None, settlement=None)` | `float` | Z-spread over curve | | `.asw_spread(curves, *, solver=None, price=None, method="par_par", settlement=None)` | `float` | Asset swap spread | | `.cs01(curves, *, solver=None)` | `float` | Credit spread sensitivity (1bp) | | `.price_yield_sensitivity(curves, *, solver=None)` | `float` | Price-yield sensitivity | | `.analytic_delta(curves, *, solver=None)` | `float` | Analytic delta | Duration `metric` accepts `"modified"` or `"macaulay"`. ASW `method` accepts `"par_par"` or `"proceeds"`. Example [#example] ```python import datetime from vade import FixedRateBond, DiscountCurve nodes = { datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97, datetime.date(2026, 1, 1): 0.94, datetime.date(2027, 1, 1): 0.91, } curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") bond = FixedRateBond( effective=datetime.date(2024, 1, 1), termination="2Y", coupon=5.0, frequency="s", convention="actactisda", ) bond.npv(curve) # net present value bond.price(curve, settlement=datetime.date(2024, 1, 2)) # clean price bond.dirty_price(curve, settlement=datetime.date(2024, 1, 2)) # dirty price bond.accrued_interest(datetime.date(2024, 3, 1)) # accrued at settlement bond.ytm(99.5, datetime.date(2024, 1, 2)) # yield to maturity bond.duration(curve, metric="modified", settlement=datetime.date(2024, 1, 2)) # modified duration ``` *** Bill [#bill] Zero-coupon bill (Treasury bill). *Python wrapper composing zero-coupon cashflow with bill analytics.* Constructor [#constructor-1] ```bash Bill( *, spec=None, effective=None, termination=None, settlement_days=1, calendar=None, currency="USD", convention="act360", face_value=100.0, disc_curve_id=None, ) ``` Parameters [#parameters-1] | Name | Type | Default | Description | | ----------------- | ------------------------- | ---------- | ------------------------------------------------------------ | | `spec` | `str` or `None` | `None` | Instrument spec name | | `effective` | `datetime.date` or `None` | `None` | Issue date | | `termination` | `date`, `str`, or `None` | `None` | Maturity date or tenor string (e.g., `"6M"`) | | `settlement_days` | `int` | `1` | Settlement lag in business days | | `calendar` | `str` or `None` | `None` | Named calendar | | `currency` | `str` | `"USD"` | Currency code | | `convention` | `str` | `"act360"` | Day count convention | | `face_value` | `float` | `100.0` | Face value | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | See [Conventions](../conventions) for accepted values for `convention`. Methods [#methods-1] | Method | Returns | Description | | -------------------------------------- | ----------- | ---------------------------- | | `.rate(curves, *, solver=None)` | `float` | Par rate against curves | | `.npv(curves, *, solver=None)` | `float` | Net present value | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | | `.price(curves, *, solver=None)` | `float` | Bill price | | `.dirty_price(curves, *, solver=None)` | `float` | Dirty price | | `.ytm(clean_price, settlement)` | `float` | Yield to maturity from price | Example [#example-1] ```python import datetime from vade import Bill, DiscountCurve nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2024, 7, 1): 0.985, datetime.date(2025, 1, 1): 0.97} curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360") bill = Bill( effective=datetime.date(2024, 1, 1), termination="6M", convention="act360", ) bill.rate(curve) # implied discount rate bill.price(curve) # bill price bill.ytm(99.0, datetime.date(2024, 1, 2)) # yield from price ``` Advanced Example [#advanced-example] ```python import datetime, math from vade import Bill, DiscountCurve, LineCurve # 6-month US Treasury Bill bill = Bill( effective=datetime.date(2024, 6, 15), termination="6m", convention="act360", currency="usd", ) # Flat 4% discount curve base = datetime.date(2024, 6, 15) nodes = {base: 1.0} for y in range(1, 6): nodes[datetime.date(2024 + y, 6, 15)] = math.exp(-0.04 * y) curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360") # Pricing price = bill.price(curve) assert 97.0 < float(price) < 100.0 dirty = bill.dirty_price(curve) assert 97.0 < float(dirty) < 100.0 # NPV npv = bill.npv(curve) assert isinstance(float(npv), float) # Cashflows -- single redemption row cf = bill.cashflows(curve) assert len(cf) >= 1 # Yield to maturity (from price and settlement date) settlement = datetime.date(2024, 6, 16) ytm = bill.ytm(float(price), settlement) assert 0.02 < float(ytm) < 0.06 # Rate (discount rate) rate = bill.rate(curve) assert isinstance(float(rate), float) # I-spread against a benchmark swap curve benchmark = LineCurve({base: 0.035, datetime.date(2030, 6, 15): 0.04}) i_spr = bill.i_spread(settlement, benchmark) assert isinstance(float(i_spr), float) ``` *** FloatRateNote [#floatratenote] Floating Rate Note (FRN). *Python wrapper composing FloatLeg with FRN analytics.* Constructor [#constructor-2] ```bash FloatRateNote( *, spec=None, effective=None, termination=None, spread=0.0, frequency="q", settlement_days=2, ex_div_days=0, calendar=None, currency="USD", convention="act360", accrued_convention=None, face_value=100.0, modifier="mf", disc_curve_id=None, ) ``` Parameters [#parameters-2] | Name | Type | Default | Description | | -------------------- | ------------------------- | ---------- | ------------------------------------------------------------------ | | `spec` | `str` or `None` | `None` | Instrument spec name | | `effective` | `datetime.date` or `None` | `None` | Issue date | | `termination` | `date`, `str`, or `None` | `None` | Maturity date or tenor string | | `spread` | `float` | `0.0` | Spread over floating rate (basis points) | | `frequency` | `str` | `"q"` | Coupon payment frequency | | `settlement_days` | `int` | `2` | Settlement lag in business days | | `ex_div_days` | `int` | `0` | Ex-dividend days before coupon payment | | `calendar` | `str` or `None` | `None` | Named calendar | | `currency` | `str` | `"USD"` | Currency code | | `convention` | `str` | `"act360"` | Day count convention | | `accrued_convention` | `str` or `None` | `None` | Separate day count for accrued interest (defaults to `convention`) | | `face_value` | `float` | `100.0` | Face value | | `modifier` | `str` | `"mf"` | Business day adjustment rule | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | See [Conventions](../conventions) for accepted values for `frequency`, `convention`, and `modifier`. Methods [#methods-2] | Method | Returns | Description | | ----------------------------------------- | ----------- | --------------------------- | | `.rate(curves, *, solver=None)` | `float` | Par rate against curves | | `.npv(curves, *, solver=None)` | `float` | Net present value | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | | `.price(curves, *, solver=None)` | `float` | Clean price | | `.dirty_price(curves, *, solver=None)` | `float` | Dirty price | | `.analytic_delta(curves, *, solver=None)` | `float` | Analytic delta | Example [#example-2] ```python import datetime from vade import FloatRateNote, DiscountCurve nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97, datetime.date(2026, 1, 1): 0.94} curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360") frn = FloatRateNote( effective=datetime.date(2024, 1, 1), termination="1Y", frequency="q", spread=50.0, convention="act360", ) frn.rate(curve) # par spread frn.npv(curve) # net present value frn.price(curve) # clean price ``` *** SubPeriodFRN [#subperiodfrn] A floating rate note where the coupon payment frequency differs from the fixing frequency. Each coupon period compounds multiple sub-period fixings: coupon = Product(1 + r\_i \* dcf\_i) - 1 + spread. *Python wrapper composing FloatLeg with sub-period compounding.* For example, a quarterly-pay / monthly-fix SubPeriodFRN has 4 coupon periods per year, but the floating rate within each coupon is compounded from \~3 monthly fixings. This is common in leveraged loan and CLO markets. Constructor [#constructor-3] ```bash SubPeriodFRN( *, effective=None, termination=None, spread=0.0, frequency="q", fixing_frequency="m", settlement_days=2, ex_div_days=0, calendar=None, currency="USD", convention="act360", accrued_convention=None, face_value=100.0, modifier="mf", disc_curve_id=None, index=None, ) ``` Parameters [#parameters-3] | Name | Type | Default | Description | | -------------------- | ------------------------- | ---------- | ------------------------------------------------------------------ | | `effective` | `datetime.date` or `None` | `None` | Issue date | | `termination` | `date`, `str`, or `None` | `None` | Maturity date or tenor string | | `spread` | `float` | `0.0` | Spread over floating rate (decimal, e.g., `0.001` = 10bp) | | `frequency` | `str` | `"q"` | Coupon payment frequency | | `fixing_frequency` | `str` | `"m"` | Sub-period fixing frequency (must be higher than `frequency`) | | `settlement_days` | `int` | `2` | Settlement lag in business days | | `ex_div_days` | `int` | `0` | Ex-dividend days before coupon payment | | `calendar` | `str` or `None` | `None` | Named calendar | | `currency` | `str` | `"USD"` | Currency code | | `convention` | `str` | `"act360"` | Day count convention | | `accrued_convention` | `str` or `None` | `None` | Separate day count for accrued interest (defaults to `convention`) | | `face_value` | `float` | `100.0` | Face value | | `modifier` | `str` | `"mf"` | Business day adjustment rule | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | | `index` | `str` or `None` | `None` | Floating rate index name for fixing lookups | See [Conventions](../conventions) for accepted values for `frequency`, `convention`, and `modifier`. Methods [#methods-3] Standard Methods [#standard-methods-1] | Method | Returns | Description | | ------------------------------------ | ----------- | --------------------------- | | `.rate(curves, *, solver=None)` | `float` | Par rate against curves | | `.npv(curves, *, solver=None)` | `float` | Net present value | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | | `.spread(curves, *, solver=None)` | `float` | Par spread | Price and Yield [#price-and-yield-1] | Method | Returns | Description | | --------------------------------------------------------- | ------- | ----------------------------------- | | `.price(curves, *, solver=None, settlement=None)` | `float` | Clean price | | `.dirty_price(curves, *, solver=None, settlement=None)` | `float` | Dirty price (clean + accrued) | | `.accrued_interest(settlement)` | `float` | Accrued interest at settlement date | | `.ytm(clean_price, settlement, convention="periodic")` | `float` | Yield to maturity from clean price | | `.price_from_ytm(ytm, settlement, convention="periodic")` | `float` | Clean price from yield to maturity | Risk Analytics [#risk-analytics-1] | Method | Returns | Description | | ------------------------------------------------------------------------------------ | ------- | --------------------------------- | | `.duration(curves, *, solver=None, metric="modified", settlement=None)` | `float` | Modified or Macaulay duration | | `.convexity(curves, *, solver=None, settlement=None)` | `float` | Price convexity | | `.dv01(curves, *, solver=None)` | `float` | Dollar value of 1bp | | `.z_spread(curves, *, solver=None, price=None, settlement=None)` | `float` | Z-spread over curve | | `.asw_spread(curves, *, solver=None, price=None, method="par_par", settlement=None)` | `float` | Asset swap spread | | `.analytic_delta(curves, *, solver=None)` | `float` | Analytic delta | | `.i_spread(settlement, swap_curve)` | `float` | Interpolated spread vs swap curve | SubPeriodFRN-Specific [#subperiodfrn-specific] | Method | Returns | Description | | ------------------------ | -------------------------------------------- | -------------------------------------------- | | `.sub_period_schedule()` | `list[tuple[date, date, date, list[tuple]]]` | Nested schedule showing sub-period structure | Each element of the outer list is a coupon period tuple: `(accrual_start, accrual_end, payment_date, sub_periods)`. Each sub-period is a tuple: `(start, end, dcf, fixing)`. Example [#example-3] ```python import datetime from vade import SubPeriodFRN, DiscountCurve # Build a discount curve nodes = { datetime.date(2024, 1, 15): 1.0, datetime.date(2025, 1, 15): 0.965, datetime.date(2026, 1, 15): 0.93, datetime.date(2030, 1, 15): 0.85, } curve = DiscountCurve(nodes, interpolation="log_linear") # 2Y quarterly-pay / monthly-fix SubPeriodFRN with 10bp spread frn = SubPeriodFRN( effective=datetime.date(2024, 1, 15), termination=datetime.date(2026, 1, 15), spread=0.001, frequency="q", fixing_frequency="m", convention="act360", ) # Price using [discount_curve, forecast_curve] list dp = frn.dirty_price(curves=[curve, curve]) assert isinstance(dp, float) assert 90 < dp < 110 # near par npv = frn.npv(curves=[curve, curve]) assert isinstance(npv, float) ``` Advanced Example [#advanced-example-1] ```python import datetime from vade import SubPeriodFRN, DiscountCurve # Build curve with several nodes for accurate forward rates nodes = { datetime.date(2024, 1, 15): 1.0, datetime.date(2024, 7, 15): 0.982, datetime.date(2025, 1, 15): 0.965, datetime.date(2025, 7, 15): 0.948, datetime.date(2026, 1, 15): 0.93, } curve = DiscountCurve(nodes, interpolation="log_linear") # 1Y SubPeriodFRN: quarterly coupon, monthly sub-period compounding frn = SubPeriodFRN( effective=datetime.date(2024, 1, 15), termination=datetime.date(2025, 1, 15), spread=0.0, frequency="q", fixing_frequency="m", convention="act360", ) # Inspect sub-period structure: 4 quarterly periods, each with ~3 monthly fixings schedule = frn.sub_period_schedule() assert len(schedule) == 4 # 4 quarterly coupon periods for accrual_start, accrual_end, payment_date, sub_periods in schedule: assert accrual_start < accrual_end # valid period assert len(sub_periods) >= 2 # at least 2 sub-period fixings per quarter for sub_start, sub_end, dcf, fixing in sub_periods: assert sub_start < sub_end assert 0 < dcf < 1 # day count fraction for a monthly period # Pricing and analytics dp = frn.dirty_price(curves=[curve, curve]) assert 90 < dp < 110 # near par for zero-spread FRN npv = frn.npv(curves=[curve, curve]) assert isinstance(npv, float) assert abs(npv - dp) < 1e-6 # NPV equals dirty price for FRN ad = frn.analytic_delta(curves=[curve, curve]) assert isinstance(ad, float) assert ad < 0 # negative delta (price falls as rates rise) ``` *** ZeroCouponBond [#zerocouponbond] Zero-coupon bond with OID (Original Issue Discount) accrued interest. *Python wrapper composing zero-coupon cashflow with compound discounting.* Accrued interest uses the OID compound method: at any settlement date, accrued equals the amortized cost (issue price compounded at yield) minus the issue price, prorated within each semi-annual period. Constructor [#constructor-4] ```bash ZeroCouponBond( *, effective=None, termination=None, issue_price=100.0, frequency="s", settlement_days=1, calendar=None, currency="USD", convention="actactisda", face_value=100.0, disc_curve_id=None, ) ``` Parameters [#parameters-4] | Name | Type | Default | Description | | ----------------- | ------------------------- | -------------- | ------------------------------------------------------------ | | `effective` | `datetime.date` or `None` | `None` | Issue date | | `termination` | `date`, `str`, or `None` | `None` | Maturity date or tenor string | | `issue_price` | `float` | `100.0` | Original issue price (OID basis for accrued calculation) | | `frequency` | `str` | `"s"` | Accrual frequency for OID calculation | | `settlement_days` | `int` | `1` | Settlement lag in business days | | `calendar` | `str` or `None` | `None` | Named calendar | | `currency` | `str` | `"USD"` | Currency code | | `convention` | `str` | `"actactisda"` | Day count convention | | `face_value` | `float` | `100.0` | Face value at maturity | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | See [Conventions](../conventions) for accepted values for `frequency` and `convention`. Methods [#methods-4] Standard Methods [#standard-methods-2] | Method | Returns | Description | | ------------------------------------ | ----------- | --------------------------- | | `.rate(curves, *, solver=None)` | `float` | Par rate against curves | | `.npv(curves, *, solver=None)` | `float` | Net present value | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | Price and Yield [#price-and-yield-2] | Method | Returns | Description | | --------------------------------------------------------- | ------- | --------------------------------------- | | `.price(curves, *, solver=None, settlement=None)` | `float` | Clean price | | `.dirty_price(curves, *, solver=None, settlement=None)` | `float` | Dirty price (clean + OID accrued) | | `.accrued_interest(settlement)` | `float` | OID accrued interest at settlement date | | `.ytm(clean_price, settlement, convention="periodic")` | `float` | Yield to maturity from clean price | | `.price_from_ytm(ytm, settlement, convention="periodic")` | `float` | Clean price from yield to maturity | Risk Analytics [#risk-analytics-2] | Method | Returns | Description | | ----------------------------------------------------------------------- | ------- | ----------------------------- | | `.duration(curves, *, solver=None, metric="modified", settlement=None)` | `float` | Modified or Macaulay duration | | `.convexity(curves, *, solver=None, settlement=None)` | `float` | Price convexity | | `.z_spread(curves, *, solver=None, price=None, settlement=None)` | `float` | Z-spread over curve | Example [#example-4] ```python import math import datetime from vade import ZeroCouponBond, DiscountCurve # Flat 4% continuously compounded curve base = datetime.date(2024, 1, 1) nodes = {base: 1.0} for y in range(1, 11): nodes[datetime.date(2024 + y, 1, 1)] = math.exp(-0.04 * y) curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") zcb = ZeroCouponBond( effective=datetime.date(2024, 1, 1), termination=datetime.date(2029, 1, 1), issue_price=80.0, face_value=100.0, frequency="s", convention="actactisda", ) settlement = datetime.date(2024, 1, 2) price = zcb.price(curve, settlement=settlement) assert 50.0 < price < 80.0 # deep discount zero-coupon bond accrued = zcb.accrued_interest(settlement) assert accrued >= 0.0 # OID accrued is non-negative ytm = zcb.ytm(float(price), settlement) assert 0.03 < ytm < 0.06 # yield in reasonable range dur = zcb.duration(curve, metric="modified", settlement=settlement) assert dur > 4.0 # near maturity duration for 5Y zero ``` Advanced Example [#advanced-example-2] ```python import datetime, math from vade import ZeroCouponBond, DiscountCurve # 5-year zero coupon bond zcb = ZeroCouponBond( effective=datetime.date(2024, 1, 15), termination="5y", face_value=100.0, frequency="s", convention="act365f", currency="usd", ) # Flat 4% discount curve base = datetime.date(2024, 1, 15) nodes = {base: 1.0} for y in range(1, 11): nodes[datetime.date(2024 + y, 1, 15)] = math.exp(-0.04 * y) curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") settlement = datetime.date(2024, 1, 16) # Pricing and yield price = zcb.price(curve, settlement=settlement) assert 75.0 < float(price) < 95.0 ytm = zcb.ytm(float(price), settlement) assert 0.02 < float(ytm) < 0.06 # Price-from-YTM roundtrip price_rt = zcb.price_from_ytm(ytm, settlement) assert abs(float(price_rt) - float(price)) < 0.01 # OID accrued interest grows over time (requires issue_price < face_value) zcb_oid = ZeroCouponBond( effective=datetime.date(2024, 1, 15), termination="5y", issue_price=80.0, face_value=100.0, frequency="s", convention="act365f", currency="usd", ) ai_early = zcb_oid.accrued_interest(datetime.date(2025, 1, 15)) ai_mid = zcb_oid.accrued_interest(datetime.date(2027, 1, 15)) assert float(ai_mid) > float(ai_early) # accrued grows over time # Cashflows -- single redemption cf = zcb.cashflows(curve) assert len(cf) >= 1 # Risk analytics dur = zcb.duration(curve, settlement=settlement) assert 3.0 < float(dur) < 6.0 conv = zcb.convexity(curve, settlement=settlement) assert float(conv) > 0 z_spr = zcb.z_spread(curve, price=float(price), settlement=settlement) assert abs(float(z_spr)) < 0.01 # near zero when priced off same curve ``` *** StepUpBond [#stepupbond] Fixed-rate bond with coupon rates that change at specified dates. *Python wrapper composing FixedLeg with dated rate resolution.* The `coupon_rates` parameter replaces the single `coupon` used by FixedRateBond, allowing step-up (or step-down) coupon schedules where the rate changes at predefined dates. Constructor [#constructor-5] ```bash StepUpBond( *, effective=None, termination=None, coupon_rates=None, frequency="s", settlement_days=1, ex_div_days=0, calendar=None, currency="USD", convention="actactisda", accrued_convention=None, face_value=100.0, modifier="none", disc_curve_id=None, ) ``` Parameters [#parameters-5] | Name | Type | Default | Description | | -------------------- | ------------------------------------ | -------------- | ------------------------------------------------------------------ | | `effective` | `datetime.date` or `None` | `None` | Issue date | | `termination` | `date`, `str`, or `None` | `None` | Maturity date or tenor string | | `coupon_rates` | `list[tuple[date, float]]` or `None` | `None` | Dated coupon rate schedule as `[(date, rate), ...]` | | `frequency` | `str` | `"s"` | Coupon payment frequency | | `settlement_days` | `int` | `1` | Settlement lag in business days | | `ex_div_days` | `int` | `0` | Ex-dividend days before coupon payment | | `calendar` | `str` or `None` | `None` | Named calendar | | `currency` | `str` | `"USD"` | Currency code | | `convention` | `str` | `"actactisda"` | Day count convention for coupon accrual | | `accrued_convention` | `str` or `None` | `None` | Separate day count for accrued interest (defaults to `convention`) | | `face_value` | `float` | `100.0` | Face value of the bond | | `modifier` | `str` | `"none"` | Business day adjustment rule | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | See [Conventions](../conventions) for accepted values for `frequency`, `convention`, and `modifier`. Methods [#methods-5] Standard Methods [#standard-methods-3] | Method | Returns | Description | | ------------------------------------ | ----------- | --------------------------- | | `.rate(curves, *, solver=None)` | `float` | Par rate against curves | | `.npv(curves, *, solver=None)` | `float` | Net present value | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | | `.spread(curves, *, solver=None)` | `float` | Par spread | Price and Yield [#price-and-yield-3] | Method | Returns | Description | | --------------------------------------------------------- | ------- | ----------------------------------- | | `.price(curves, *, solver=None, settlement=None)` | `float` | Clean price | | `.dirty_price(curves, *, solver=None, settlement=None)` | `float` | Dirty price (clean + accrued) | | `.accrued_interest(settlement)` | `float` | Accrued interest at settlement date | | `.ytm(clean_price, settlement, convention="periodic")` | `float` | Yield to maturity from clean price | | `.price_from_ytm(ytm, settlement, convention="periodic")` | `float` | Clean price from yield to maturity | Risk Analytics [#risk-analytics-3] | Method | Returns | Description | | ------------------------------------------------------------------------------------ | ------- | ----------------------------- | | `.duration(curves, *, solver=None, metric="modified", settlement=None)` | `float` | Modified or Macaulay duration | | `.convexity(curves, *, solver=None, settlement=None)` | `float` | Price convexity | | `.dv01(curves, *, solver=None)` | `float` | Dollar value of 1bp | | `.z_spread(curves, *, solver=None, price=None, settlement=None)` | `float` | Z-spread over curve | | `.asw_spread(curves, *, solver=None, price=None, method="par_par", settlement=None)` | `float` | Asset swap spread | | `.analytic_delta(curves, *, solver=None)` | `float` | Analytic delta | Example [#example-5] ```python import math import datetime from vade import StepUpBond, DiscountCurve # Flat 4% continuously compounded curve base = datetime.date(2024, 1, 1) nodes = {base: 1.0} for y in range(1, 11): nodes[datetime.date(2024 + y, 1, 1)] = math.exp(-0.04 * y) curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") bond = StepUpBond( effective=datetime.date(2024, 1, 1), termination=datetime.date(2029, 1, 1), coupon_rates=[ (datetime.date(2024, 1, 1), 0.03), # 3% for first 3 years (datetime.date(2027, 1, 1), 0.05), # steps up to 5% ], frequency="s", face_value=100.0, convention="actactisda", ) settlement = datetime.date(2024, 1, 2) price = bond.price(curve, settlement=settlement) assert 90.0 < price < 110.0 # near par ytm = bond.ytm(float(price), settlement) assert 0.03 < ytm < 0.06 # yield between step-up rates dur = bond.duration(curve, metric="modified", settlement=settlement) assert 3.0 < dur < 5.5 # reasonable duration for 5Y bond ``` Advanced Example [#advanced-example-3] ```python import datetime, math from vade import StepUpBond, DiscountCurve # 7-year step-up bond with 3 coupon steps sub = StepUpBond( effective=datetime.date(2024, 3, 15), termination="7y", coupon_rates=[ (datetime.date(2024, 3, 15), 0.03), # 3.0% initial (datetime.date(2026, 3, 15), 0.035), # steps to 3.5% at year 2 (datetime.date(2028, 3, 15), 0.04), # steps to 4.0% at year 4 (datetime.date(2030, 3, 15), 0.045), # steps to 4.5% at year 6 ], frequency="s", convention="act365f", currency="usd", ) # Flat 4% discount curve base = datetime.date(2024, 3, 15) nodes = {base: 1.0} for y in range(1, 11): nodes[datetime.date(2024 + y, 3, 15)] = math.exp(-0.04 * y) curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") # Pricing price = sub.price(curve) assert 90.0 < float(price) < 110.0 dirty = sub.dirty_price(curve) assert 90.0 < float(dirty) < 115.0 # Yield to maturity settlement = datetime.date(2024, 3, 16) ytm = sub.ytm(float(price), settlement) assert 0.02 < float(ytm) < 0.06 # Cashflows -- shows varying coupon amounts across steps cf = sub.cashflows(curve) assert len(cf) > 10 # semi-annual over 7 years # Risk analytics dur = sub.duration(curve) assert 3.0 < float(dur) < 8.0 conv = sub.convexity(curve) assert float(conv) > 0 dv01 = sub.dv01(curve) assert float(dv01) > 0 z_spr = sub.z_spread(curve, price=float(price)) assert abs(float(z_spr)) < 0.01 # near zero when priced off same curve ``` *** AmortizingBond [#amortizingbond] Fixed-rate bond with declining notional via scheduled principal repayments. *Python wrapper composing FixedLeg with amortization schedule.* Principal repayment amounts are specified as absolute values. Coupon payments are computed on the remaining (reduced) notional after each principal repayment. Constructor [#constructor-6] ```bash AmortizingBond( *, effective=None, termination=None, coupon=0.0, amortization_schedule=None, frequency="s", settlement_days=1, ex_div_days=0, calendar=None, currency="USD", convention="actactisda", accrued_convention=None, face_value=100.0, modifier="none", disc_curve_id=None, ) ``` Parameters [#parameters-6] | Name | Type | Default | Description | | ----------------------- | ------------------------------------ | -------------- | ------------------------------------------------------------------ | | `effective` | `datetime.date` or `None` | `None` | Issue date | | `termination` | `date`, `str`, or `None` | `None` | Maturity date or tenor string | | `coupon` | `float` | `0.0` | Coupon rate (percentage, e.g., `0.05` for 5%) | | `amortization_schedule` | `list[tuple[date, float]]` or `None` | `None` | Principal repayment schedule as `[(date, amount), ...]` | | `frequency` | `str` | `"s"` | Coupon payment frequency | | `settlement_days` | `int` | `1` | Settlement lag in business days | | `ex_div_days` | `int` | `0` | Ex-dividend days before coupon payment | | `calendar` | `str` or `None` | `None` | Named calendar | | `currency` | `str` | `"USD"` | Currency code | | `convention` | `str` | `"actactisda"` | Day count convention for coupon accrual | | `accrued_convention` | `str` or `None` | `None` | Separate day count for accrued interest (defaults to `convention`) | | `face_value` | `float` | `100.0` | Initial face value of the bond | | `modifier` | `str` | `"none"` | Business day adjustment rule | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | See [Conventions](../conventions) for accepted values for `frequency`, `convention`, and `modifier`. Methods [#methods-6] Standard Methods [#standard-methods-4] | Method | Returns | Description | | ------------------------------------ | ----------- | --------------------------- | | `.rate(curves, *, solver=None)` | `float` | Par rate against curves | | `.npv(curves, *, solver=None)` | `float` | Net present value | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | | `.spread(curves, *, solver=None)` | `float` | Par spread | Price and Yield [#price-and-yield-4] | Method | Returns | Description | | --------------------------------------------------------- | ------- | ----------------------------------- | | `.price(curves, *, solver=None, settlement=None)` | `float` | Clean price | | `.dirty_price(curves, *, solver=None, settlement=None)` | `float` | Dirty price (clean + accrued) | | `.accrued_interest(settlement)` | `float` | Accrued interest at settlement date | | `.ytm(clean_price, settlement, convention="periodic")` | `float` | Yield to maturity from clean price | | `.price_from_ytm(ytm, settlement, convention="periodic")` | `float` | Clean price from yield to maturity | Risk Analytics [#risk-analytics-4] | Method | Returns | Description | | ------------------------------------------------------------------------------------ | ------- | ----------------------------- | | `.duration(curves, *, solver=None, metric="modified", settlement=None)` | `float` | Modified or Macaulay duration | | `.convexity(curves, *, solver=None, settlement=None)` | `float` | Price convexity | | `.dv01(curves, *, solver=None)` | `float` | Dollar value of 1bp | | `.z_spread(curves, *, solver=None, price=None, settlement=None)` | `float` | Z-spread over curve | | `.asw_spread(curves, *, solver=None, price=None, method="par_par", settlement=None)` | `float` | Asset swap spread | | `.analytic_delta(curves, *, solver=None)` | `float` | Analytic delta | Example [#example-6] ```python import math import datetime from vade import AmortizingBond, DiscountCurve # Flat 4% continuously compounded curve base = datetime.date(2024, 1, 1) nodes = {base: 1.0} for y in range(1, 11): nodes[datetime.date(2024 + y, 1, 1)] = math.exp(-0.04 * y) curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") bond = AmortizingBond( effective=datetime.date(2024, 1, 1), termination=datetime.date(2029, 1, 1), coupon=0.05, amortization_schedule=[ (datetime.date(2026, 1, 1), 25.0), # repay 25 at year 2 (datetime.date(2028, 1, 1), 25.0), # repay 25 at year 4 ], frequency="a", face_value=100.0, convention="actactisda", ) settlement = datetime.date(2024, 1, 2) price = bond.price(curve, settlement=settlement) assert 95.0 < price < 115.0 # above par when coupon > yield ytm = bond.ytm(float(price), settlement) assert 0.03 < ytm < 0.06 # reasonable yield range # Accrued interest reflects current notional ai_early = bond.accrued_interest(datetime.date(2024, 7, 1)) ai_late = bond.accrued_interest(datetime.date(2026, 7, 1)) assert ai_early > ai_late # lower notional after amortization ``` Advanced Example [#advanced-example-4] ```python import datetime, math from vade import AmortizingBond, DiscountCurve # 5-year amortizing bond: 25% principal at Y2 and Y4 ab = AmortizingBond( effective=datetime.date(2024, 3, 15), termination="5y", coupon=0.05, frequency="s", convention="act365f", currency="usd", amortization_schedule=[ (datetime.date(2026, 3, 15), 25.0), (datetime.date(2028, 3, 15), 25.0), ], ) # Flat 4% discount curve base = datetime.date(2024, 3, 15) nodes = {base: 1.0} for y in range(1, 11): nodes[datetime.date(2024 + y, 3, 15)] = math.exp(-0.04 * y) curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") # Pricing and yield settlement = datetime.date(2024, 3, 16) price = ab.price(curve) assert 100.0 < float(price) < 110.0 # above par, coupon > discount rate ytm = ab.ytm(float(price), settlement) assert 0.03 < float(ytm) < 0.06 # Cashflows -- inspect types: "Amortizing" and "Principal" rows present cf = ab.cashflows(curve) assert len(cf) > 5 cf_types = cf["Type"].to_list() assert "Amortizing" in cf_types assert "Principal" in cf_types # amortization creates principal repayment rows # Accrued interest declines as notional amortizes ai_before = ab.accrued_interest(datetime.date(2025, 6, 15)) # before any amort ai_after = ab.accrued_interest(datetime.date(2026, 6, 15)) # after first 25% amort assert float(ai_before) > float(ai_after) # lower notional -> lower accrued # Risk analytics -- amortization reduces effective duration dur = ab.duration(curve) assert 2.0 < float(dur) < 5.0 # shorter than 5Y bullet conv = ab.convexity(curve) assert float(conv) > 0 dv01 = ab.dv01(curve) assert float(dv01) > 0 z_spr = ab.z_spread(curve, price=float(price)) assert abs(float(z_spr)) < 0.01 # near zero when priced off same curve ``` *** PIKBond [#pikbond] Payment-in-kind bond where accrued interest compounds into a growing notional. *Python wrapper composing FixedLeg with PIK notional accretion.* With `pik_fraction=1.0` (full PIK), no cash coupons are paid; all interest accrues to principal. With `pik_fraction=0.5`, half the coupon is paid in cash and half compounds into the notional. The `notional_schedule()` method returns the growing notional at each period boundary. Constructor [#constructor-7] ```bash PIKBond( *, effective=None, termination=None, coupon=0.0, pik_fraction=1.0, frequency="s", settlement_days=1, ex_div_days=0, calendar=None, currency="USD", convention="actactisda", accrued_convention=None, face_value=100.0, modifier="none", disc_curve_id=None, ) ``` Parameters [#parameters-7] | Name | Type | Default | Description | | -------------------- | ------------------------- | -------------- | -------------------------------------------------------------------- | | `effective` | `datetime.date` or `None` | `None` | Issue date | | `termination` | `date`, `str`, or `None` | `None` | Maturity date or tenor string | | `coupon` | `float` | `0.0` | Coupon rate (percentage, e.g., `0.06` for 6%) | | `pik_fraction` | `float` | `1.0` | Fraction of coupon paid in kind (`1.0` = full PIK, `0.0` = all cash) | | `frequency` | `str` | `"s"` | Coupon payment frequency | | `settlement_days` | `int` | `1` | Settlement lag in business days | | `ex_div_days` | `int` | `0` | Ex-dividend days before coupon payment | | `calendar` | `str` or `None` | `None` | Named calendar | | `currency` | `str` | `"USD"` | Currency code | | `convention` | `str` | `"actactisda"` | Day count convention for coupon accrual | | `accrued_convention` | `str` or `None` | `None` | Separate day count for accrued interest (defaults to `convention`) | | `face_value` | `float` | `100.0` | Initial face value of the bond | | `modifier` | `str` | `"none"` | Business day adjustment rule | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | See [Conventions](../conventions) for accepted values for `frequency`, `convention`, and `modifier`. Methods [#methods-7] Standard Methods [#standard-methods-5] | Method | Returns | Description | | ------------------------------------ | ----------- | --------------------------- | | `.rate(curves, *, solver=None)` | `float` | Par rate against curves | | `.npv(curves, *, solver=None)` | `float` | Net present value | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | | `.spread(curves, *, solver=None)` | `float` | Par spread | Price and Yield [#price-and-yield-5] | Method | Returns | Description | | --------------------------------------------------------- | ------- | ----------------------------------- | | `.price(curves, *, solver=None, settlement=None)` | `float` | Clean price | | `.dirty_price(curves, *, solver=None, settlement=None)` | `float` | Dirty price (clean + accrued) | | `.accrued_interest(settlement)` | `float` | Accrued interest at settlement date | | `.ytm(clean_price, settlement, convention="periodic")` | `float` | Yield to maturity from clean price | | `.price_from_ytm(ytm, settlement, convention="periodic")` | `float` | Clean price from yield to maturity | Risk Analytics [#risk-analytics-5] | Method | Returns | Description | | ------------------------------------------------------------------------------------ | ------- | ----------------------------- | | `.duration(curves, *, solver=None, metric="modified", settlement=None)` | `float` | Modified or Macaulay duration | | `.convexity(curves, *, solver=None, settlement=None)` | `float` | Price convexity | | `.dv01(curves, *, solver=None)` | `float` | Dollar value of 1bp | | `.z_spread(curves, *, solver=None, price=None, settlement=None)` | `float` | Z-spread over curve | | `.asw_spread(curves, *, solver=None, price=None, method="par_par", settlement=None)` | `float` | Asset swap spread | | `.analytic_delta(curves, *, solver=None)` | `float` | Analytic delta | PIK-Specific [#pik-specific] | Method | Returns | Description | | ---------------------- | -------------------------- | ---------------------------------------- | | `.notional_schedule()` | `list[tuple[date, float]]` | Growing notional at each period boundary | Example [#example-7] ```python import math import datetime from vade import PIKBond, DiscountCurve # Flat 4% continuously compounded curve base = datetime.date(2024, 1, 1) nodes = {base: 1.0} for y in range(1, 11): nodes[datetime.date(2024 + y, 1, 1)] = math.exp(-0.04 * y) curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") bond = PIKBond( effective=datetime.date(2024, 1, 1), termination=datetime.date(2029, 1, 1), coupon=0.06, pik_fraction=1.0, # full PIK: all interest compounds frequency="a", face_value=100.0, convention="actactisda", ) # Full PIK means no cash coupon accrues assert bond.accrued_interest(datetime.date(2024, 7, 1)) == 0.0 # Notional grows each period schedule = bond.notional_schedule() assert schedule[0][1] == 100.0 # initial face value assert schedule[1][1] > 100.0 # notional grows after first period price = bond.price(curve, settlement=datetime.date(2024, 1, 2)) assert 100.0 < price < 120.0 # above par for 6% PIK vs 4% discount ``` Advanced Example [#advanced-example-5] ```python import datetime, math from vade import PIKBond, DiscountCurve # Full PIK bond: 6% coupon, all interest capitalised pik_full = PIKBond( effective=datetime.date(2024, 1, 15), termination="5y", coupon=0.06, pik_fraction=1.0, frequency="a", convention="act365f", currency="usd", ) # Partial PIK bond: same terms, 50% cash / 50% PIK pik_partial = PIKBond( effective=datetime.date(2024, 1, 15), termination="5y", coupon=0.06, pik_fraction=0.5, frequency="a", convention="act365f", currency="usd", ) # Flat 4% discount curve base = datetime.date(2024, 1, 15) nodes = {base: 1.0} for y in range(1, 11): nodes[datetime.date(2024 + y, 1, 15)] = math.exp(-0.04 * y) curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") # notional_schedule() shows compounding growth for full PIK ns = pik_full.notional_schedule() assert len(ns) > 1 # First entry is par, subsequent entries grow as interest is capitalised assert ns[0][1] == 100.0 assert ns[-1][1] > 100.0 # notional has grown # Full PIK has zero accrued (all capitalised) ai_full = pik_full.accrued_interest(datetime.date(2024, 7, 15)) assert float(ai_full) == 0.0 # Partial PIK has positive accrued (cash portion) ai_partial = pik_partial.accrued_interest(datetime.date(2024, 7, 15)) assert float(ai_partial) > 0.0 # Full PIK price is higher (more future cashflows from compounding) price_full = pik_full.price(curve) price_partial = pik_partial.price(curve) assert float(price_full) > float(price_partial) # Cashflows cf = pik_full.cashflows(curve) assert len(cf) > 0 # Risk analytics dur = pik_full.duration(curve) assert 3.0 < float(dur) < 6.0 conv = pik_full.convexity(curve) assert float(conv) > 0 ``` *** CappedFloatRateNote [#cappedfloatratenote] Floating rate note with embedded cap and/or floor on the coupon rate. *Python wrapper composing FloatLeg with Black-76 or Bachelier cap/floor pricing.* Requires `vol` (volatility) -- there is no intrinsic-only fallback. At least one of `cap_rate` or `floor_rate` must be specified. When both are set, the instrument is a collared FRN. Optionally supports sub-period compounding via `fixing_frequency`. Constructor [#constructor-8] ```bash CappedFloatRateNote( *, effective=None, termination=None, spread=0.0, frequency="q", fixing_frequency=None, cap_rate=None, floor_rate=None, vol=None, vol_model="black76", settlement_days=2, ex_div_days=0, calendar=None, currency="USD", convention="act360", accrued_convention=None, face_value=100.0, modifier="mf", disc_curve_id=None, ) ``` Parameters [#parameters-8] | Name | Type | Default | Description | | -------------------- | ------------------------- | ----------- | -------------------------------------------------------------------------- | | `effective` | `datetime.date` or `None` | `None` | Issue date | | `termination` | `date`, `str`, or `None` | `None` | Maturity date or tenor string | | `spread` | `float` | `0.0` | Spread over floating rate (decimal) | | `frequency` | `str` | `"q"` | Coupon payment frequency | | `fixing_frequency` | `str` or `None` | `None` | Sub-period fixing frequency (e.g., `"m"` for monthly within quarterly) | | `cap_rate` | `float` or `None` | `None` | Cap strike rate (decimal). At least one of cap\_rate/floor\_rate required. | | `floor_rate` | `float` or `None` | `None` | Floor strike rate (decimal) | | `vol` | `float` or `None` | `None` | Volatility for option pricing. **Required** -- no intrinsic fallback. | | `vol_model` | `str` | `"black76"` | Pricing model: `"black76"` or `"bachelier"` | | `settlement_days` | `int` | `2` | Settlement lag in business days | | `ex_div_days` | `int` | `0` | Ex-dividend days before coupon payment | | `calendar` | `str` or `None` | `None` | Named calendar | | `currency` | `str` | `"USD"` | Currency code | | `convention` | `str` | `"act360"` | Day count convention | | `accrued_convention` | `str` or `None` | `None` | Separate day count for accrued interest (defaults to `convention`) | | `face_value` | `float` | `100.0` | Face value | | `modifier` | `str` | `"mf"` | Business day adjustment rule | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | See [Conventions](../conventions) for accepted values for `frequency`, `convention`, and `modifier`. Methods [#methods-8] Standard Methods [#standard-methods-6] | Method | Returns | Description | | ------------------------------------ | ----------- | --------------------------- | | `.rate(curves, *, solver=None)` | `float` | Par rate against curves | | `.npv(curves, *, solver=None)` | `float` | Net present value | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | | `.spread(curves, *, solver=None)` | `float` | Par spread | Price and Yield [#price-and-yield-6] | Method | Returns | Description | | --------------------------------------------------------- | ------- | ----------------------------------- | | `.price(curves, *, solver=None, settlement=None)` | `float` | Clean price | | `.dirty_price(curves, *, solver=None, settlement=None)` | `float` | Dirty price (clean + accrued) | | `.accrued_interest(settlement)` | `float` | Accrued interest at settlement date | | `.ytm(clean_price, settlement, convention="periodic")` | `float` | Yield to maturity from clean price | | `.price_from_ytm(ytm, settlement, convention="periodic")` | `float` | Clean price from yield to maturity | Risk Analytics [#risk-analytics-6] | Method | Returns | Description | | ------------------------------------------------------------------------------------ | ------- | ----------------------------- | | `.duration(curves, *, solver=None, metric="modified", settlement=None)` | `float` | Modified or Macaulay duration | | `.convexity(curves, *, solver=None, settlement=None)` | `float` | Price convexity | | `.dv01(curves, *, solver=None)` | `float` | Dollar value of 1bp | | `.z_spread(curves, *, solver=None, price=None, settlement=None)` | `float` | Z-spread over curve | | `.asw_spread(curves, *, solver=None, price=None, method="par_par", settlement=None)` | `float` | Asset swap spread | | `.analytic_delta(curves, *, solver=None)` | `float` | Analytic delta | CappedFRN-Specific [#cappedfrn-specific] | Method | Returns | Description | | ------------------------ | ----------- | ----------------------------------------------- | | `.sub_period_schedule()` | `DataFrame` | Sub-period schedule when `fixing_frequency` set | Example [#example-8] ```python import math import datetime from vade import CappedFloatRateNote, FloatRateNote, DiscountCurve # Steep discount curve (rates ~5-6%) base = datetime.date(2024, 1, 1) nodes = {base: 1.0} for y in range(1, 11): nodes[datetime.date(2024 + y, 1, 1)] = math.exp(-0.055 * y) curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") # Uncapped FRN for comparison frn = FloatRateNote( effective=datetime.date(2024, 1, 1), termination=datetime.date(2026, 1, 1), spread=0.0, frequency="q", convention="act360", ) frn_price = frn.dirty_price(curves=[curve, curve]) # Capped FRN with 2% cap (well below market rates) capped = CappedFloatRateNote( effective=datetime.date(2024, 1, 1), termination=datetime.date(2026, 1, 1), spread=0.0, frequency="q", cap_rate=0.02, vol=0.20, convention="act360", ) capped_price = capped.dirty_price(curves=[curve, curve]) assert isinstance(capped_price, float) assert capped_price < frn_price # cap reduces value to investor ``` Advanced Example [#advanced-example-6] ```python import datetime, math from vade import CappedFloatRateNote, DiscountCurve # Base discount/forecast curve (flat 4%) base = datetime.date(2024, 6, 15) nodes = {base: 1.0} for y in range(1, 6): nodes[datetime.date(2024 + y, 6, 15)] = math.exp(-0.04 * y) curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360") # Capped FRN: floating rate capped at 5% capped = CappedFloatRateNote( effective=datetime.date(2024, 6, 15), termination="3y", frequency="q", convention="act360", currency="usd", cap_rate=0.05, vol=0.50, ) # Floored FRN: floating rate floored at 2% floored = CappedFloatRateNote( effective=datetime.date(2024, 6, 15), termination="3y", frequency="q", convention="act360", currency="usd", floor_rate=0.02, vol=0.50, ) # Collared FRN: both cap and floor collared = CappedFloatRateNote( effective=datetime.date(2024, 6, 15), termination="3y", frequency="q", convention="act360", currency="usd", cap_rate=0.05, floor_rate=0.02, vol=0.50, ) # Pricing -- pass curves as list [discount, forecast] (required format) price_capped = capped.dirty_price(curves=[curve, curve]) price_floored = floored.dirty_price(curves=[curve, curve]) price_collared = collared.dirty_price(curves=[curve, curve]) assert isinstance(float(price_capped), float) assert isinstance(float(price_floored), float) assert isinstance(float(price_collared), float) # Floor adds value (guarantees minimum coupon) # Cap reduces value (limits upside) # So: capped < collared < floored assert float(price_capped) < float(price_floored) # Bachelier model comparison (vol_model="bachelier") capped_bach = CappedFloatRateNote( effective=datetime.date(2024, 6, 15), termination="3y", frequency="q", convention="act360", currency="usd", cap_rate=0.05, vol=0.01, # Bachelier vol in absolute terms vol_model="bachelier", ) price_bach = capped_bach.dirty_price(curves=[curve, curve]) assert isinstance(float(price_bach), float) # Bachelier and Black-76 produce different prices assert float(price_bach) != float(price_capped) # Cashflows cf = capped.cashflows(curves=[curve, curve]) assert len(cf) > 0 # Risk analytics -- near-zero for FRN is expected (resets to par) dur = capped.duration(curves=[curve, curve]) assert isinstance(float(dur), float) ``` *** AssetSwap [#assetswap] Asset swap decomposing a bond into a floating-rate equivalent. *Python wrapper composing bond + floating leg.* Supports `"par"` (par-par) and `"non_par"` asset swap types. The par asset swap computes the spread over floating that makes the package NPV zero at par. The non-par variant requires a `dirty_price` input. See also: [Spread Analytics Guide](../guides/credit/spread-analytics) for asset swap workflows and par vs non-par comparison. Constructor [#constructor-9] ```bash AssetSwap(bond, *, asw_type="par", dirty_price=None, spread=None, disc_curve_id=None) ``` Parameters [#parameters-9] | Name | Type | Default | Description | | --------------- | ----------------- | ---------- | ----------------------------------------------------------------------------- | | `bond` | bond instrument | *required* | Underlying bond (FixedRateBond, FloatRateNote, StepUpBond, etc.). Positional. | | `asw_type` | `str` | `"par"` | Asset swap type: `"par"` or `"non_par"` | | `dirty_price` | `float` or `None` | `None` | Dirty price of the bond (required for `"non_par"`) | | `spread` | `float` or `None` | `None` | Fixed spread override | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | Methods [#methods-9] | Method | Returns | Description | | ------------------------------------ | ----------- | -------------------------------- | | `.rate(curves, *, solver=None)` | `float` | Asset swap spread | | `.npv(curves, *, solver=None)` | `float` | Net present value of the package | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | Example [#example-9] ```python import math import datetime from vade import AssetSwap, FixedRateBond, DiscountCurve # Flat 3% continuously compounded curve base = datetime.date(2024, 1, 1) nodes = {base: 1.0} for y in range(1, 11): nodes[datetime.date(2024 + y, 1, 1)] = math.exp(-0.03 * y) curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f", id="disc") bond = FixedRateBond( effective=datetime.date(2024, 1, 1), termination="5Y", coupon=0.04, frequency="s", face_value=100.0, ) # Par asset swap asw = AssetSwap(bond=bond, asw_type="par") spread = asw.rate(curves=curve) assert isinstance(spread, float) assert -0.5 < spread < 0.5 # spread in reasonable range # NPV is zero at fair spread (no fixed spread override) npv = asw.npv(curves=curve) assert abs(npv) < 1e-8 # Non-par asset swap with explicit dirty price asw_np = AssetSwap(bond=bond, asw_type="non_par", dirty_price=102.5) spread_np = asw_np.rate(curves=curve) assert isinstance(spread_np, float) ``` *** CallableBond [#callablebond] Bond with embedded call and/or put options, priced via Hull-White trinomial tree. *Python wrapper composing bond + option schedule.* Tree-based methods require Hull-White model parameters: `a` (mean reversion speed) and `sigma` (short-rate volatility). Standard bond methods (rate, npv, cashflows) delegate to the underlying bond. See also: [Callable Bonds Guide](../guides/credit/callable-bonds) for OAS analysis and effective duration workflows. Constructor [#constructor-10] ```bash CallableBond(bond, *, call_schedule=None, put_schedule=None, disc_curve_id=None) ``` Parameters [#parameters-10] | Name | Type | Default | Description | | --------------- | ------------------------------------ | ---------- | ------------------------------------------------------------ | | `bond` | bond instrument | *required* | Underlying bond (typically FixedRateBond). Positional. | | `call_schedule` | `list[tuple[date, float]]` or `None` | `None` | Call dates and prices as `[(date, strike_price), ...]` | | `put_schedule` | `list[tuple[date, float]]` or `None` | `None` | Put dates and prices as `[(date, strike_price), ...]` | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | Methods [#methods-10] Standard Methods (delegated to underlying bond) [#standard-methods-delegated-to-underlying-bond] | Method | Returns | Description | | ------------------------------------ | ----------- | --------------------------- | | `.rate(curves, *, solver=None)` | `float` | Par rate against curves | | `.npv(curves, *, solver=None)` | `float` | Net present value | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | Tree-Based Methods (Hull-White) [#tree-based-methods-hull-white] | Method | Returns | Description | | ----------------------------------------------------------------- | ------- | ---------------------------------------- | | `.tree_price(curves, a, sigma, *, spread=0.0)` | `float` | Option-adjusted price via trinomial tree | | `.oas(curves, market_price, a, sigma)` | `float` | Option-adjusted spread from market price | | `.effective_duration(curves, a, sigma, *, oas=0.0, bump=0.0001)` | `float` | Effective duration (curve bump) | | `.effective_convexity(curves, a, sigma, *, oas=0.0, bump=0.0001)` | `float` | Effective convexity (curve bump) | | `.vega(curves, a, sigma, *, oas=0.0, bump=0.0001)` | `float` | Vega (price sensitivity to volatility) | Tree-based methods take `a` (mean reversion speed, e.g., `0.1`) and `sigma` (short-rate vol, e.g., `0.01`). Example [#example-10] ```python import math import datetime from vade import CallableBond, FixedRateBond, DiscountCurve # Flat 3% continuously compounded curve base = datetime.date(2024, 1, 1) nodes = {base: 1.0} for y in range(1, 11): nodes[datetime.date(2024 + y, 1, 1)] = math.exp(-0.03 * y) curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f", id="disc") bond = FixedRateBond( effective=datetime.date(2024, 1, 1), termination="5Y", coupon=0.05, frequency="a", face_value=100.0, ) cb = CallableBond( bond=bond, call_schedule=[ (datetime.date(2026, 1, 1), 100.0), (datetime.date(2027, 1, 1), 100.0), (datetime.date(2028, 1, 1), 100.0), ], ) # Tree price with Hull-White parameters tree_px = cb.tree_price(curves=curve, a=0.1, sigma=0.01) assert 80.0 < tree_px < 120.0 # reasonable price range # OAS: use tree_price as market price => OAS should be ~0 oas = cb.oas(curves=curve, market_price=tree_px, a=0.1, sigma=0.01) assert abs(oas) < 1e-4 # OAS ~0 when market = model price # Effective duration and convexity eff_dur = cb.effective_duration(curves=curve, a=0.1, sigma=0.01) assert eff_dur > 0 # positive duration eff_cvx = cb.effective_convexity(curves=curve, a=0.1, sigma=0.01) assert isinstance(eff_cvx, float) ``` *** CDS [#cds] Credit Default Swap with hazard rate curve pricing. *Rust-backed.* See also: [Credit Curves & CDS Guide](../guides/credit/credit-curves-cds) for CDS-based credit curve calibration. **Alias:** `CDS = CreditDefaultSwap` Constructor [#constructor-11] ```bash CDS( *, spec=None, effective=None, termination=None, frequency="q", notional=10_000_000.0, recovery_rate=0.4, fixed_rate=100.0, convention="act360", currency="USD", premium_accrued=True, modifier="mf", stub="shortfront", ) ``` Parameters [#parameters-11] | Name | Type | Default | Description | | ----------------- | ------------------------- | -------------- | ---------------------------------------------------- | | `spec` | `str` or `None` | `None` | Instrument spec name | | `effective` | `datetime.date` or `None` | `None` | Protection start date | | `termination` | `date`, `str`, or `None` | `None` | Protection end date or tenor string (e.g., `"5Y"`) | | `frequency` | `str` | `"q"` | Premium payment frequency | | `notional` | `float` | `10_000_000.0` | Notional amount | | `recovery_rate` | `float` | `0.4` | Assumed recovery rate (decimal, e.g., `0.4` for 40%) | | `fixed_rate` | `float` | `100.0` | Fixed premium rate (basis points) | | `convention` | `str` | `"act360"` | Day count convention | | `currency` | `str` | `"USD"` | Currency code | | `premium_accrued` | `bool` | `True` | Whether premium accrues to default date | | `modifier` | `str` | `"mf"` | Business day adjustment rule | | `stub` | `str` | `"shortfront"` | Stub period preference | See [Conventions](../conventions) for accepted values for `frequency`, `convention`, `modifier`, and `stub`. Methods [#methods-11] CDS methods take two separate curve arguments: a discount curve and a [CreditImpliedCurve](curves#creditimpliedcurve) for survival probabilities. | Method | Returns | Description | | ---------------------------------------------- | ----------- | --------------------------------- | | `.rate(disc_curve, credit_curve)` | `float` | Par spread (same as `.spread`) | | `.npv(disc_curve, credit_curve)` | `float` | Net present value | | `.spread(disc_curve, credit_curve)` | `float` | Par spread | | `.cashflows(disc_curve, credit_curve)` | `DataFrame` | Period-level cashflow table | | `.accrued(today=None)` | `float` | Accrued premium | | `.analytic_rec_risk(disc_curve, credit_curve)` | `float` | Recovery risk | | `.jtd(today=None)` | `float` | Jump-to-default exposure | | `.ead()` | `float` | Exposure at default | | `.upfront(disc_curve, credit_curve)` | `float` | Upfront payment amount | | `.to_upfront_spread(disc_curve, credit_curve)` | `float` | Convert running to upfront spread | Example [#example-11] ```python import datetime from vade import CDS, DiscountCurve, CreditImpliedCurve disc_nodes = { datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97, datetime.date(2026, 1, 1): 0.94, datetime.date(2029, 1, 1): 0.85, } disc_curve = DiscountCurve(disc_nodes, interpolation="log_linear", convention="act365f") credit_nodes = { datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.99, datetime.date(2026, 1, 1): 0.98, datetime.date(2029, 1, 1): 0.95, } credit_curve = CreditImpliedCurve(credit_nodes, convention="act365f", recovery_rate=0.4) cds = CDS( effective=datetime.date(2024, 1, 1), termination="5Y", fixed_rate=100.0, frequency="q", convention="act360", recovery_rate=0.4, ) cds.npv(disc_curve, credit_curve) # net present value cds.spread(disc_curve, credit_curve) # par spread in basis points cds.ead() # exposure at default ``` *** *** See Also [#see-also] * [Credit Guides](../../guides/credit/) -- Bond analytics, CDS pricing, fitted curves, and spread analysis guides * [Curves API](../rates/curves) -- CreditImpliedCurve, FittedBondCurve, SpreadCurve # Curves (/docs/api/rates/curves) Discount factor, zero rate, forward rate, spread, composite, parametric, and implied curve classes. All types are available via flat import: ```bash from vade import DiscountCurve, LineCurve, ForwardCurve, SpreadCurve, CompositeCurve, TurnOfYearCurve, FXImpliedCurve, NelsonSiegel, NelsonSiegelSvensson, SmithWilson, IRImpliedCurve, CreditImpliedCurve, FittedBondCurve ``` See [Conventions](../conventions) for all accepted string parameter values. **Contents:** [DiscountCurve](#discountcurve) | [LineCurve](#linecurve) | [ForwardCurve](#forwardcurve) | [SpreadCurve](#spreadcurve) | [CompositeCurve](#compositecurve) | [TurnOfYearCurve](#turnofyearcurve) | [FXImpliedCurve](#fximpliedcurve) | [NelsonSiegel](#nelsonsiegel) | [NelsonSiegelSvensson](#nelsonsiegelsvensson) | [SmithWilson](#smithwilson) | [IRImpliedCurve](#irimpliedcurve) | [CreditImpliedCurve](#creditimpliedcurve) | [FittedBondCurve](#fittedbondcurve) See also: [Curve Building Guide](../guides/rates/curve-building) for end-to-end curve construction workflows. *** DiscountCurve [#discountcurve] Discount factor curve with interpolated DF values. *Rust-backed.* Constructor [#constructor] ```bash DiscountCurve(nodes, *, interpolation="log_linear", id="curve", convention="act360", modifier="modified_following", calendar=None, t=None, endpoints="not_a_knot") ``` Parameters [#parameters] | Name | Type | Default | Description | | --------------- | ------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------- | | `nodes` | `NodeValues` | *required* | Discount factor nodes as `{date: float}` dict or `([dates], [values])` tuple. Positional-only. | | `interpolation` | `str` | `"log_linear"` | Interpolation method for discount factors | | `id` | `str` | `"curve"` | Curve identifier | | `convention` | `str` | `"act360"` | Day count convention for rate calculations | | `modifier` | `str` | `"modified_following"` | Business day adjustment rule | | `calendar` | `BusinessCalendar` or `None` | `None` | Business day [calendar](calendar#businesscalendar) | | `t` | `list[datetime.date]` or `None` | `None` | Knot dates for B-spline interpolation | | `endpoints` | `str` | `"not_a_knot"` | Endpoint condition for spline methods | `NodeValues` accepts `Dict[datetime.date, float | Dual | Dual2]` or `Tuple[List[datetime.date], List[float | Dual | Dual2]]`. See [Conventions](../conventions#interpolation-methods) for accepted `interpolation` values. See [Conventions](../conventions#day-count-conventions) for accepted `convention` values. Methods [#methods] | Method | Returns | Description | | --------------------------- | ------------------------ | ----------------------------------------------- | | `.discount_factor(date)` | `float \| Dual \| Dual2` | Discount factor at date | | `.zero_rate(start, end)` | `float \| Dual \| Dual2` | Continuously compounded zero rate between dates | | `.forward_rate(start, end)` | `float \| Dual \| Dual2` | Forward rate between dates | | `.shift(bp)` | `DiscountCurve` | Parallel shift by bp basis points | | `.translate(days)` | `DiscountCurve` | Translate curve forward by N days | | `.roll(days)` | `DiscountCurve` | Roll curve forward (time decay) | | `.to_json()` | `str` | Serialize to JSON string | | `.from_json(s)` (static) | `DiscountCurve` | Deserialize from JSON string | Return types are `Union[float, Dual, Dual2]` -- see the [type system guide](../getting-started/type-system#union-return-types) for when each variant appears. Example [#example] ```python import datetime from vade import DiscountCurve nodes = { datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97, datetime.date(2026, 1, 1): 0.94, } curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") df = curve.discount_factor(datetime.date(2024, 7, 1)) assert 0.97 < df < 1.0 # interpolated mid-year discount factor zr = curve.zero_rate(datetime.date(2024, 1, 1), datetime.date(2025, 1, 1)) assert 0.02 < zr < 0.04 # zero rate implied by DF=0.97 shifted = curve.shift(10) # shift by 10 basis points ``` *** LineCurve [#linecurve] Value-based curve for zero rates, spreads, or other quantities. *Rust-backed.* Constructor [#constructor-1] ```bash LineCurve(nodes, *, interpolation="linear", id="curve", convention="act360", modifier="modified_following", calendar=None, t=None, endpoints="not_a_knot") ``` Parameters [#parameters-1] | Name | Type | Default | Description | | --------------- | ------------------------------- | ---------------------- | ------------------------------------------------------------------------------------ | | `nodes` | `NodeValues` | *required* | Value nodes as `{date: float}` dict or `([dates], [values])` tuple. Positional-only. | | `interpolation` | `str` | `"linear"` | Interpolation method | | `id` | `str` | `"curve"` | Curve identifier | | `convention` | `str` | `"act360"` | Day count convention | | `modifier` | `str` | `"modified_following"` | Business day adjustment rule | | `calendar` | `BusinessCalendar` or `None` | `None` | Business day [calendar](calendar#businesscalendar) | | `t` | `list[datetime.date]` or `None` | `None` | Knot dates for B-spline interpolation | | `endpoints` | `str` | `"not_a_knot"` | Endpoint condition for spline methods | See [Conventions](../conventions#interpolation-methods) for accepted `interpolation` values. Methods [#methods-1] | Method | Returns | Description | | --------------------------- | ------------------------ | ---------------------------------------------------- | | `.discount_factor(date)` | `float \| Dual \| Dual2` | Discount factor at date (derived from stored values) | | `.zero_rate(start, end)` | `float \| Dual \| Dual2` | Zero rate between dates | | `.forward_rate(start, end)` | `float \| Dual \| Dual2` | Forward rate between dates | | `.shift(bp)` | `LineCurve` | Parallel shift by bp basis points | | `.translate(days)` | `LineCurve` | Translate curve forward by N days | | `.roll(days)` | `LineCurve` | Roll curve forward (time decay) | | `.to_json()` | `str` | Serialize to JSON string | | `.from_json(s)` (static) | `LineCurve` | Deserialize from JSON string | Example [#example-1] ```python import datetime from vade import LineCurve nodes = { datetime.date(2024, 1, 1): 0.030, datetime.date(2025, 1, 1): 0.035, datetime.date(2026, 1, 1): 0.040, } curve = LineCurve(nodes, interpolation="linear", convention="act365f") zr = curve.zero_rate(datetime.date(2024, 1, 1), datetime.date(2024, 7, 1)) assert 0.03 <= zr <= 0.035 # interpolated mid-year rate fr = curve.forward_rate(datetime.date(2024, 1, 1), datetime.date(2025, 1, 1)) assert fr > 0 # positive forward rate ``` *** ForwardCurve [#forwardcurve] Forward rate curve with native forward rate representation. *Rust-backed.* Nodes represent instantaneous forward rates (not discount factors). Uses Gauss-Legendre quadrature for discount factor integration. Constructor [#constructor-2] ```bash ForwardCurve(nodes, *, interpolation="flat_forward", id="forward_curve", convention="act365f", modifier="modified_following", calendar=None, t=None, endpoints="not_a_knot") ``` Parameters [#parameters-2] | Name | Type | Default | Description | | --------------- | ------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------- | | `nodes` | `NodeValues` | *required* | Forward rate nodes as `{date: float}` dict or `([dates], [values])` tuple. Positional-only. | | `interpolation` | `str` | `"flat_forward"` | Interpolation method for forward rates | | `id` | `str` | `"forward_curve"` | Curve identifier | | `convention` | `str` | `"act365f"` | Day count convention | | `modifier` | `str` | `"modified_following"` | Business day adjustment rule | | `calendar` | `BusinessCalendar` or `None` | `None` | Business day [calendar](calendar#businesscalendar) | | `t` | `list[datetime.date]` or `None` | `None` | Knot dates for B-spline interpolation | | `endpoints` | `str` | `"not_a_knot"` | Endpoint condition for spline methods | See [Conventions](../conventions#interpolation-methods) for accepted `interpolation` values. Methods [#methods-2] | Method | Returns | Description | | --------------------------- | ------------------------ | ------------------------------------------------------- | | `.discount_factor(date)` | `float \| Dual \| Dual2` | Discount factor at date (integrated from forward rates) | | `.zero_rate(start, end)` | `float \| Dual \| Dual2` | Zero rate between dates | | `.forward_rate(start, end)` | `float \| Dual \| Dual2` | Forward rate between dates | | `.shift(bp)` | `ForwardCurve` | Parallel shift by bp basis points | | `.translate(days)` | `ForwardCurve` | Translate curve forward by N days | | `.roll(days)` | `ForwardCurve` | Roll curve forward (time decay) | | `.to_json()` | `str` | Serialize to JSON string | | `.from_json(s)` (static) | `ForwardCurve` | Deserialize from JSON string | Example [#example-2] ```python import datetime from vade import ForwardCurve # Instantaneous forward rates (per-day units matching internal x-axis) nodes = { datetime.date(2024, 1, 1): 8.2e-5, datetime.date(2025, 1, 1): 9.0e-5, datetime.date(2026, 1, 1): 1.0e-4, } curve = ForwardCurve(nodes, interpolation="flat_forward", convention="act365f") df = curve.discount_factor(datetime.date(2025, 1, 1)) assert 0.9 < df < 1.0 # discount factor integrated from forward rates fr = curve.forward_rate(datetime.date(2024, 1, 1), datetime.date(2025, 1, 1)) assert 0.02 < fr < 0.04 # annualized forward rate ``` *** SpreadCurve [#spreadcurve] Spread curve: base curve plus additive or multiplicative spread. *Rust-backed.* The base curve is passed as a JSON string (from `curve.to_json()`). Spread nodes are zero-rate spreads at specified dates. Constructor [#constructor-3] ```bash SpreadCurve(base_json, spread_nodes, *, mode="additive", spread_interpolation="linear", convention="act365f", id="spread_curve") ``` Parameters [#parameters-3] | Name | Type | Default | Description | | ---------------------- | ------------ | ---------------- | -------------------------------------------------------------------------------------- | | `base_json` | `str` | *required* | JSON string of the base curve (from `.to_json()`). Positional-only. | | `spread_nodes` | `NodeValues` | *required* | Spread values as `{date: float}` dict or `([dates], [values])` tuple. Positional-only. | | `mode` | `str` | `"additive"` | Spread mode: `"additive"` or `"multiplicative"` | | `spread_interpolation` | `str` | `"linear"` | Interpolation method for spread values | | `convention` | `str` | `"act365f"` | Day count convention | | `id` | `str` | `"spread_curve"` | Curve identifier | Methods [#methods-3] | Method | Returns | Description | | --------------------------- | ------------------------ | --------------------------------------- | | `.discount_factor(date)` | `float \| Dual \| Dual2` | Discount factor at date (base + spread) | | `.zero_rate(start, end)` | `float \| Dual \| Dual2` | Zero rate between dates | | `.forward_rate(start, end)` | `float \| Dual \| Dual2` | Forward rate between dates | | `.to_json()` | `str` | Serialize to JSON string | | `.from_json(s)` (static) | `SpreadCurve` | Deserialize from JSON string | Note: SpreadCurve does not support `.shift()`, `.translate()`, or `.roll()`. Example [#example-3] ```python import datetime from vade import DiscountCurve, SpreadCurve base_nodes = { datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97, datetime.date(2026, 1, 1): 0.94, } base = DiscountCurve(base_nodes, interpolation="log_linear", convention="act365f") base_json = base.to_json() spread_nodes = { datetime.date(2024, 1, 1): 0.001, datetime.date(2025, 1, 1): 0.002, datetime.date(2026, 1, 1): 0.003, } spread = SpreadCurve(base_json, spread_nodes, mode="additive") df = spread.discount_factor(datetime.date(2025, 1, 1)) assert 0.9 < df < 1.0 # spread-adjusted discount factor ``` Advanced Example [#advanced-example] Multiplicative mode and JSON roundtrip: ```python import datetime from vade import DiscountCurve, SpreadCurve # Base OIS curve with realistic discount factors base = DiscountCurve( { datetime.date(2025, 6, 16): 1.0, datetime.date(2026, 6, 16): 0.9615, datetime.date(2027, 6, 16): 0.9240, datetime.date(2028, 6, 16): 0.8880, }, interpolation="log_linear", convention="act360", ) # Credit spread term structure (in zero-rate space) spread_nodes = { datetime.date(2025, 6, 16): 0.005, datetime.date(2026, 6, 16): 0.008, datetime.date(2027, 6, 16): 0.012, datetime.date(2028, 6, 16): 0.015, } # Additive spread: zero_rate = base_zero_rate + spread additive = SpreadCurve(base.to_json(), spread_nodes, mode="additive") df_add = additive.discount_factor(datetime.date(2027, 6, 16)) assert 0.85 < float(df_add) < 0.95 # lower DF than base due to spread # Multiplicative spread: discount_factor = base_df * spread_df multiplicative = SpreadCurve(base.to_json(), spread_nodes, mode="multiplicative") df_mult = multiplicative.discount_factor(datetime.date(2027, 6, 16)) assert 0.85 < float(df_mult) < 0.95 # JSON roundtrip preserves all parameters restored = SpreadCurve.from_json(additive.to_json()) df_restored = restored.discount_factor(datetime.date(2027, 6, 16)) assert abs(float(df_restored) - float(df_add)) < 1e-12 ``` > **See also:** [Curve Building Guide](../guides/rates/curve-building) for spread curve usage in multi-curve frameworks. *** CompositeCurve [#compositecurve] Composite curve combining multiple curves. *Rust-backed.* Discount factors are computed as the product of discount factors from all component curves. Constructor [#constructor-4] ```bash CompositeCurve(curves, id=None) ``` Parameters [#parameters-4] | Name | Type | Default | Description | | -------- | --------------- | ---------- | -------------------------------- | | `curves` | `list` | *required* | List of curve objects to combine | | `id` | `str` or `None` | `None` | Curve identifier | Methods [#methods-4] | Method | Returns | Description | | --------------------------- | ------------------------ | --------------------------------------------- | | `.discount_factor(date)` | `float \| Dual \| Dual2` | Product of component discount factors at date | | `.zero_rate(start, end)` | `float \| Dual \| Dual2` | Zero rate between dates | | `.forward_rate(start, end)` | `float \| Dual \| Dual2` | Forward rate between dates | | `.shift(bp)` | `CompositeCurve` | Parallel shift by bp basis points | | `.translate(days)` | `CompositeCurve` | Translate curve forward by N days | | `.to_json()` | `str` | Serialize to JSON string | | `.from_json(s)` (static) | `CompositeCurve` | Deserialize from JSON string | Note: CompositeCurve does not support `.roll()`. Example [#example-4] ```python import datetime from vade import DiscountCurve, CompositeCurve nodes1 = { datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.98, datetime.date(2026, 1, 1): 0.96, } nodes2 = { datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.99, datetime.date(2026, 1, 1): 0.98, } curve1 = DiscountCurve(nodes1, convention="act365f") curve2 = DiscountCurve(nodes2, convention="act365f") composite = CompositeCurve([curve1, curve2]) df = composite.discount_factor(datetime.date(2025, 1, 1)) assert 0.96 < df < 0.98 # product of 0.98 * 0.99 ``` Advanced Example [#advanced-example-1] DF multiplicativity proof and IRS pricing with a composite funding curve: ```python import datetime from vade import DiscountCurve, CompositeCurve, IRS # OIS discount curve (risk-free rate ~4%) ois = DiscountCurve( {datetime.date(2025, 6, 16): 1.0, datetime.date(2026, 6, 16): 0.9615, datetime.date(2027, 6, 16): 0.9240}, convention="act360", id="ois", ) # Funding spread curve (credit spread ~20bps) spread = DiscountCurve( {datetime.date(2025, 6, 16): 1.0, datetime.date(2026, 6, 16): 0.998, datetime.date(2027, 6, 16): 0.996}, convention="act360", id="spread", ) # Composite = OIS x Spread (DF is product of components) funding = CompositeCurve([ois, spread], id="funding") df_composite = funding.discount_factor(datetime.date(2026, 6, 16)) df_ois = ois.discount_factor(datetime.date(2026, 6, 16)) df_spread = spread.discount_factor(datetime.date(2026, 6, 16)) assert abs(float(df_composite) - float(df_ois) * float(df_spread)) < 1e-10 # Use composite curve for IRS discounting irs = IRS( effective=datetime.date(2025, 6, 16), termination="2y", frequency="s", convention="act360", fixed_rate=4.0, notional=10_000_000, currency="usd", ) npv = irs.npv(funding) assert isinstance(float(npv), float) ``` *** TurnOfYearCurve [#turnofyearcurve] Turn-of-year adjusted curve with date-specific bumps. *Rust-backed.* The inner curve is passed as a JSON string (from `curve.to_json()`). Bumps are `{date: bp_adjustment}` for turn-of-year effects. Constructor [#constructor-5] ```bash TurnOfYearCurve(inner_json, bumps, *, convention="act365f", calendar=None, id="toy_curve") ``` Parameters [#parameters-5] | Name | Type | Default | Description | | ------------ | ---------------------------- | ------------- | -------------------------------------------------------------------- | | `inner_json` | `str` | *required* | JSON string of the inner curve (from `.to_json()`). Positional-only. | | `bumps` | `Dict[datetime.date, float]` | *required* | Date-specific basis point adjustments. Positional-only. | | `convention` | `str` | `"act365f"` | Day count convention | | `calendar` | `BusinessCalendar` or `None` | `None` | Business day [calendar](calendar#businesscalendar) | | `id` | `str` | `"toy_curve"` | Curve identifier | Methods [#methods-5] | Method | Returns | Description | | --------------------------- | ------------------------ | --------------------------------------- | | `.discount_factor(date)` | `float \| Dual \| Dual2` | Discount factor at date (inner + bumps) | | `.zero_rate(start, end)` | `float \| Dual \| Dual2` | Zero rate between dates | | `.forward_rate(start, end)` | `float \| Dual \| Dual2` | Forward rate between dates | | `.to_json()` | `str` | Serialize to JSON string | | `.from_json(s)` (static) | `TurnOfYearCurve` | Deserialize from JSON string | Note: TurnOfYearCurve does not support `.shift()`, `.translate()`, or `.roll()`. Example [#example-5] ```python import datetime from vade import DiscountCurve, TurnOfYearCurve nodes = { datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97, datetime.date(2026, 1, 1): 0.94, } inner = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") inner_json = inner.to_json() bumps = { datetime.date(2024, 12, 31): 5.0, datetime.date(2025, 12, 31): 3.0, } toy = TurnOfYearCurve(inner_json, bumps, convention="act365f") df = toy.discount_factor(datetime.date(2025, 1, 1)) assert 0.9 < df < 1.0 # adjusted discount factor ``` Advanced Example [#advanced-example-2] Year-end effect on discount factors and JSON roundtrip: ```python import datetime from vade import DiscountCurve, TurnOfYearCurve # Smooth underlying curve inner = DiscountCurve( { datetime.date(2025, 6, 16): 1.0, datetime.date(2026, 6, 16): 0.9615, datetime.date(2027, 6, 16): 0.9240, datetime.date(2028, 6, 16): 0.8880, }, interpolation="log_linear", convention="act365f", ) # Year-end bumps in basis points (5bp at 2025 year-end, 3bp at 2026 year-end) bumps = { datetime.date(2025, 12, 31): 5.0, datetime.date(2026, 12, 31): 3.0, } toy = TurnOfYearCurve(inner.to_json(), bumps, convention="act365f") # Discount factors reflect year-end bumps df_after_ye = toy.discount_factor(datetime.date(2026, 1, 2)) inner_df_after = inner.discount_factor(datetime.date(2026, 1, 2)) # TurnOfYear curve produces lower DFs than inner curve around year-end assert float(df_after_ye) < float(inner_df_after) # JSON roundtrip preserves bumps restored = TurnOfYearCurve.from_json(toy.to_json()) df_check = restored.discount_factor(datetime.date(2026, 1, 2)) assert abs(float(df_check) - float(df_after_ye)) < 1e-12 ``` > **See also:** [Curve Building Guide](../guides/rates/curve-building) for incorporating turn-of-year effects in curve construction. *** FXImpliedCurve [#fximpliedcurve] Proxy curve for FX-adjusted discounting. *Rust-backed.* Used in cross-currency frameworks where discounting in one currency is proxied through another via FX rates and interest rate differentials. See also: [FX Rates & Forwards Guide](../guides/fx/fx-rates-forwards) for FX implied curve construction. Constructor [#constructor-6] ```bash FXImpliedCurve(cashflow_ccy, collateral_ccy, fx_rates, fx_settlements, collateral_curve, cashflow_curve, id=None) ``` Parameters [#parameters-6] | Name | Type | Default | Description | | ------------------ | -------------------------- | ---------- | ------------------------------------------ | | `cashflow_ccy` | `str` | *required* | Currency of the cashflows (e.g., `"eur"`) | | `collateral_ccy` | `str` | *required* | Currency of the collateral (e.g., `"usd"`) | | `fx_rates` | `FXRates` | *required* | FX rates object for currency conversion | | `fx_settlements` | `Dict[str, datetime.date]` | *required* | Settlement dates per currency | | `collateral_curve` | `DiscountCurve` | *required* | Discount curve in collateral currency | | `cashflow_curve` | `DiscountCurve` | *required* | Discount curve in cashflow currency | | `id` | `str` or `None` | `None` | Curve identifier | Methods [#methods-6] | Method | Returns | Description | | --------------------------- | ------------------------ | ----------------------------------- | | `.discount_factor(date)` | `float \| Dual \| Dual2` | FX-adjusted discount factor at date | | `.zero_rate(start, end)` | `float \| Dual \| Dual2` | Zero rate between dates | | `.forward_rate(start, end)` | `float \| Dual \| Dual2` | Forward rate between dates | | `.shift(bp)` | `FXImpliedCurve` | Parallel shift by bp basis points | | `.translate(days)` | `FXImpliedCurve` | Translate curve forward by N days | | `.to_json()` | `str` | Serialize to JSON string | | `.from_json(s)` (static) | `FXImpliedCurve` | Deserialize from JSON string | Note: FXImpliedCurve does not support `.roll()`. Example [#example-6] ```python import datetime from vade import DiscountCurve, FXRates, FXImpliedCurve usd_nodes = { datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.96, datetime.date(2026, 1, 1): 0.92, } eur_nodes = { datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97, datetime.date(2026, 1, 1): 0.94, } usd_curve = DiscountCurve(usd_nodes, convention="act365f", id="usd") eur_curve = DiscountCurve(eur_nodes, convention="act365f", id="eur") fx = FXRates({"eurusd": 1.10}, datetime.date(2024, 1, 1)) settlements = {"usd": datetime.date(2024, 1, 3), "eur": datetime.date(2024, 1, 3)} fx_implied = FXImpliedCurve("eur", "usd", fx, settlements, usd_curve, eur_curve) df = fx_implied.discount_factor(datetime.date(2025, 1, 1)) assert 0.9 < float(df) < 1.0 # FX-adjusted discount factor (returns Dual due to FXRates AD) ``` *** NelsonSiegel [#nelsonsiegel] Nelson-Siegel parametric yield curve model. *Rust-backed.* See also: [Bootstrap & Parametric Guide](../guides/rates/bootstrap-parametric) for parametric curve fitting workflows. Models the yield curve as a function of three factors (level, slope, curvature) with a single decay parameter tau. Constructor [#constructor-7] ```bash NelsonSiegel(beta0, beta1, beta2, tau, base_date, convention="act365f", id="") ``` Parameters [#parameters-7] | Name | Type | Default | Description | | ------------ | ------------------------ | ----------- | ---------------------------- | | `beta0` | `float \| Dual \| Dual2` | *required* | Level (long-term rate) | | `beta1` | `float \| Dual \| Dual2` | *required* | Slope (short-term component) | | `beta2` | `float \| Dual \| Dual2` | *required* | Curvature (medium-term hump) | | `tau` | `float \| Dual \| Dual2` | *required* | Decay parameter | | `base_date` | `datetime.date` | *required* | Base date for the curve | | `convention` | `str` | `"act365f"` | Day count convention | | `id` | `str` | `""` | Curve identifier | Methods [#methods-7] | Method | Returns | Description | | --------------------------- | ------------------------ | --------------------------------- | | `.discount_factor(date)` | `float \| Dual \| Dual2` | Discount factor at date | | `.zero_rate(start, end)` | `float \| Dual \| Dual2` | Zero rate between dates | | `.forward_rate(start, end)` | `float \| Dual \| Dual2` | Forward rate between dates | | `.shift(bp)` | `NelsonSiegel` | Parallel shift by bp basis points | | `.translate(days)` | `NelsonSiegel` | Translate curve forward by N days | | `.roll(days)` | `NelsonSiegel` | Roll curve forward (time decay) | | `.to_json()` | `str` | Serialize to JSON string | | `.from_json(s)` (static) | `NelsonSiegel` | Deserialize from JSON string | Example [#example-7] ```python import datetime from vade import NelsonSiegel ns = NelsonSiegel( beta0=0.05, beta1=-0.02, beta2=0.01, tau=1.5, base_date=datetime.date(2024, 1, 1), convention="act365f", ) df = ns.discount_factor(datetime.date(2025, 1, 1)) assert 0.9 < df < 1.0 # discount factor from parametric model zr = ns.zero_rate(datetime.date(2024, 1, 1), datetime.date(2034, 1, 1)) assert 0.04 < zr < 0.06 # long-term rate converges to beta0 ``` *** NelsonSiegelSvensson [#nelsonsiegelsvensson] Nelson-Siegel-Svensson parametric yield curve model. *Rust-backed.* Extends Nelson-Siegel with a second curvature term and decay parameter for more flexible curve shapes. Constructor [#constructor-8] ```bash NelsonSiegelSvensson(beta0, beta1, beta2, beta3, tau1, tau2, base_date, convention="act365f", id="") ``` Parameters [#parameters-8] | Name | Type | Default | Description | | ------------ | ------------------------ | ----------- | ---------------------------- | | `beta0` | `float \| Dual \| Dual2` | *required* | Level (long-term rate) | | `beta1` | `float \| Dual \| Dual2` | *required* | Slope (short-term component) | | `beta2` | `float \| Dual \| Dual2` | *required* | First curvature term | | `beta3` | `float \| Dual \| Dual2` | *required* | Second curvature term | | `tau1` | `float \| Dual \| Dual2` | *required* | First decay parameter | | `tau2` | `float \| Dual \| Dual2` | *required* | Second decay parameter | | `base_date` | `datetime.date` | *required* | Base date for the curve | | `convention` | `str` | `"act365f"` | Day count convention | | `id` | `str` | `""` | Curve identifier | Methods [#methods-8] | Method | Returns | Description | | --------------------------- | ------------------------ | --------------------------------- | | `.discount_factor(date)` | `float \| Dual \| Dual2` | Discount factor at date | | `.zero_rate(start, end)` | `float \| Dual \| Dual2` | Zero rate between dates | | `.forward_rate(start, end)` | `float \| Dual \| Dual2` | Forward rate between dates | | `.shift(bp)` | `NelsonSiegelSvensson` | Parallel shift by bp basis points | | `.translate(days)` | `NelsonSiegelSvensson` | Translate curve forward by N days | | `.roll(days)` | `NelsonSiegelSvensson` | Roll curve forward (time decay) | | `.to_json()` | `str` | Serialize to JSON string | | `.from_json(s)` (static) | `NelsonSiegelSvensson` | Deserialize from JSON string | Example [#example-8] ```python import datetime from vade import NelsonSiegelSvensson nss = NelsonSiegelSvensson( beta0=0.05, beta1=-0.02, beta2=0.01, beta3=0.005, tau1=1.5, tau2=5.0, base_date=datetime.date(2024, 1, 1), convention="act365f", ) df = nss.discount_factor(datetime.date(2025, 1, 1)) assert 0.9 < df < 1.0 # discount factor from parametric model zr = nss.zero_rate(datetime.date(2024, 1, 1), datetime.date(2034, 1, 1)) assert 0.04 < zr < 0.06 # long-term rate converges to beta0 ``` *** SmithWilson [#smithwilson] Smith-Wilson parametric curve for regulatory yield curve extrapolation. *Rust-backed.* Used in insurance regulation (e.g., Solvency II) for extrapolating yield curves beyond the last liquid point toward an ultimate forward rate (UFR). Constructor [#constructor-9] ```bash SmithWilson(dates, rates, ufr, alpha, base_date, convention="act365f", id="") ``` Parameters [#parameters-9] | Name | Type | Default | Description | | ------------ | --------------------- | ----------- | --------------------------------------------- | | `dates` | `list[datetime.date]` | *required* | Knot dates for observed rates | | `rates` | `list[float]` | *required* | Observed zero rates at knot dates | | `ufr` | `float` | *required* | Ultimate forward rate (long-term target rate) | | `alpha` | `float` | *required* | Speed of convergence to UFR | | `base_date` | `datetime.date` | *required* | Base date for the curve | | `convention` | `str` | `"act365f"` | Day count convention | | `id` | `str` | `""` | Curve identifier | Methods [#methods-9] | Method | Returns | Description | | --------------------------- | ------------------------ | --------------------------------- | | `.discount_factor(date)` | `float \| Dual \| Dual2` | Discount factor at date | | `.zero_rate(start, end)` | `float \| Dual \| Dual2` | Zero rate between dates | | `.forward_rate(start, end)` | `float \| Dual \| Dual2` | Forward rate between dates | | `.shift(bp)` | `SmithWilson` | Parallel shift by bp basis points | | `.translate(days)` | `SmithWilson` | Translate curve forward by N days | | `.roll(days)` | `SmithWilson` | Roll curve forward (time decay) | | `.to_json()` | `str` | Serialize to JSON string | | `.from_json(s)` (static) | `SmithWilson` | Deserialize from JSON string | Example [#example-9] ```python import datetime from vade import SmithWilson sw = SmithWilson( dates=[ datetime.date(2025, 1, 1), datetime.date(2029, 1, 1), datetime.date(2034, 1, 1), datetime.date(2044, 1, 1), ], rates=[0.030, 0.032, 0.035, 0.038], ufr=0.035, alpha=0.1, base_date=datetime.date(2024, 1, 1), convention="act365f", ) df = sw.discount_factor(datetime.date(2029, 1, 1)) assert 0.8 < df < 1.0 # discount factor from Smith-Wilson model zr = sw.zero_rate(datetime.date(2024, 1, 1), datetime.date(2029, 1, 1)) assert 0.02 < zr < 0.05 # zero rate at 5-year point ``` *** IRImpliedCurve [#irimpliedcurve] Implied curve for bucket-level delta risk reporting. *Rust-backed.* Constructs a curve from a source [DiscountCurve](#discountcurve) with custom tenor buckets. Used to compute bucket-level risk sensitivities. See also: [Risk Guide](../guides/rates/risk) for IRImpliedCurve usage in delta/gamma computation. Constructor [#constructor-10] ```bash IRImpliedCurve(source, ref_date, tenors, calendar, convention="act365f") ``` Parameters [#parameters-10] | Name | Type | Default | Description | | ------------ | ------------------ | ----------- | --------------------------------------------------------------- | | `source` | `DiscountCurve` | *required* | Source discount curve to re-bucket | | `ref_date` | `datetime.date` | *required* | Reference date for tenor resolution | | `tenors` | `list[str]` | *required* | Tenor bucket labels (e.g., `["1Y", "2Y", "5Y", "10Y"]`) | | `calendar` | `BusinessCalendar` | *required* | [Calendar](calendar#businesscalendar) for tenor date resolution | | `convention` | `str` | `"act365f"` | Day count convention | See [Conventions](../conventions#tenor-strings) for tenor string format. Methods [#methods-10] | Method | Returns | Description | | --------------------------- | ------------------------ | ------------------------------------------------- | | `.discount_factor(date)` | `float \| Dual \| Dual2` | Discount factor at date | | `.zero_rate(start, end)` | `float \| Dual \| Dual2` | Zero rate between dates | | `.forward_rate(start, end)` | `float \| Dual \| Dual2` | Forward rate between dates | | `.bucket_delta(instrument)` | `polars.DataFrame` | Compute bucket-level delta risk for an instrument | | `.tenor_labels()` | `list[str]` | Return the tenor bucket labels | Note: IRImpliedCurve does not support `.shift()`, `.translate()`, `.roll()`, `.to_json()`, or `.from_json()`. Example [#example-10] ```python import datetime from vade import DiscountCurve, IRImpliedCurve, BusinessCalendar nodes = { datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97, datetime.date(2026, 1, 1): 0.94, datetime.date(2029, 1, 1): 0.85, datetime.date(2034, 1, 1): 0.72, } source = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") implied = IRImpliedCurve( source=source, ref_date=datetime.date(2024, 1, 1), tenors=["1Y", "2Y", "5Y", "10Y"], calendar=BusinessCalendar("NYC"), ) implied.tenor_labels() # ['1Y', '2Y', '5Y', '10Y'] df = implied.discount_factor(datetime.date(2025, 1, 1)) assert 0.96 < df < 0.98 # consistent with source curve ``` *** CreditImpliedCurve [#creditimpliedcurve] Credit implied curve with piecewise flat hazard rates. *Rust-backed.* Models credit risk through survival probabilities derived from hazard rates. Used for CDS pricing and credit risk analysis. See also: [Credit Curves & CDS Guide](../guides/credit/credit-curves-cds) for credit curve calibration workflows. Constructor [#constructor-11] ```bash CreditImpliedCurve(nodes, convention="act360", recovery_rate=0.4, id="credit_curve") ``` Parameters [#parameters-11] | Name | Type | Default | Description | | --------------- | ------------ | ---------------- | --------------------------------------------------------------------------------- | | `nodes` | `NodeValues` | *required* | Survival probability nodes as `{date: float}` dict or `([dates], [values])` tuple | | `convention` | `str` | `"act360"` | Day count convention | | `recovery_rate` | `float` | `0.4` | Recovery rate assumption (0.0 to 1.0) | | `id` | `str` | `"credit_curve"` | Curve identifier | Properties [#properties] | Property | Type | Description | | ---------------- | ------- | ------------------------ | | `.id` | `str` | Curve identifier | | `.recovery_rate` | `float` | Recovery rate assumption | Methods [#methods-11] | Method | Returns | Description | | ----------------------------- | ------------------------ | --------------------------------- | | `.survival_probability(date)` | `float \| Dual \| Dual2` | Survival probability at date | | `.discount_factor(date)` | `float \| Dual \| Dual2` | Discount factor at date | | `.zero_rate(start, end)` | `float \| Dual \| Dual2` | Zero rate between dates | | `.forward_rate(start, end)` | `float \| Dual \| Dual2` | Forward rate between dates | | `.shift(bp)` | `CreditImpliedCurve` | Parallel shift by bp basis points | | `.translate(days)` | `CreditImpliedCurve` | Translate curve forward by N days | | `.to_json()` | `str` | Serialize to JSON string | | `.from_json(s)` (static) | `CreditImpliedCurve` | Deserialize from JSON string | Note: CreditImpliedCurve does not support `.roll()`. Example [#example-11] ```python import datetime from vade import CreditImpliedCurve nodes = { datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.98, datetime.date(2026, 1, 1): 0.95, datetime.date(2029, 1, 1): 0.88, } credit = CreditImpliedCurve(nodes, convention="act360", recovery_rate=0.4) sp = credit.survival_probability(datetime.date(2025, 1, 1)) assert 0.0 < sp < 1.0 # survival probability at 1 year credit.id # 'credit_curve' credit.recovery_rate # 0.4 ``` *** FittedBondCurve [#fittedbondcurve] Parametric curve fitted to bond market prices via nonlinear least-squares. *Python wrapper composing NS/NSS/SmithWilson fitting.* Fits a [NelsonSiegel](#nelsonsiegel), [NelsonSiegelSvensson](#nelsonsiegelsvensson), or [SmithWilson](#smithwilson) curve to observed bond clean prices. Uses Levenberg-Marquardt optimization with inverse-duration weighting. See also: [Fitted Curves Guide](../guides/credit/fitted-curves) for end-to-end fitting workflows. Constructor [#constructor-12] ```bash FittedBondCurve(bonds, clean_prices, settlement, model="NS", *, convention="act365f", base_date=None, weights=None, initial_params=None, max_iter=200, func_tol=1e-10, ufr=None, alpha=None, id="") ``` Parameters [#parameters-12] | Name | Type | Default | Description | | ---------------- | ------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------- | | `bonds` | `list[FixedRateBond]` | *required* | Portfolio of bonds to fit. Positional. | | `clean_prices` | `list[float]` | *required* | Observed clean prices for each bond. Positional. | | `settlement` | `datetime.date` | *required* | Settlement/valuation date. Positional. | | `model` | `str` | `"NS"` | Model type: `"NS"` (Nelson-Siegel), `"NSS"` (Nelson-Siegel-Svensson), or `"SW"` (Smith-Wilson). Positional. | | `convention` | `str` | `"act365f"` | Day count convention for rate calculations | | `base_date` | `datetime.date` or `None` | `None` | Base date for curve (defaults to settlement) | | `weights` | `list[float]` or `None` | `None` | Custom fitting weights (overrides inverse-duration default) | | `initial_params` | `list[float]` or `None` | `None` | Initial parameter guess for optimizer | | `max_iter` | `int` | `200` | Maximum Levenberg-Marquardt iterations | | `func_tol` | `float` | `1e-10` | Function tolerance for convergence | | `ufr` | `float` or `None` | `None` | Ultimate forward rate (required for SW model) | | `alpha` | `float` or `None` | `None` | Convergence speed (required for SW model) | | `id` | `str` | `""` | Curve identifier | Properties [#properties-1] | Property | Type | Description | | ------------- | ------- | ------------------------------ | | `.rmse` | `float` | Root mean square pricing error | | `.converged` | `bool` | Whether optimization converged | | `.iterations` | `int` | Number of iterations used | | `.objective` | `float` | Final objective function value | Methods [#methods-12] | Method | Returns | Description | | --------------------------- | ----------------------------------------------------- | ---------------------------------------------------- | | `.discount_factor(date)` | `float` | Discount factor at date | | `.zero_rate(start, end)` | `float` | Zero rate between dates | | `.forward_rate(start, end)` | `float` | Forward rate between dates | | `.pricing_errors()` | `list[float]` | Per-bond pricing errors (model price - market price) | | `.curve()` | `NelsonSiegel \| NelsonSiegelSvensson \| SmithWilson` | Extract the fitted inner curve | Example [#example-12] ```python import datetime from vade import FittedBondCurve, FixedRateBond, NelsonSiegel settlement = datetime.date(2024, 1, 15) # Create a portfolio of bonds with different maturities bonds = [] for maturity_year, coupon in [(2026, 0.03), (2027, 0.035), (2029, 0.04), (2031, 0.042), (2034, 0.045)]: bond = FixedRateBond( effective=settlement, termination=datetime.date(maturity_year, 1, 15), frequency="s", convention="act365f", coupon=coupon, face_value=100.0, ) bonds.append(bond) # Observed clean prices clean_prices = [99.5, 99.0, 98.5, 97.0, 95.0] # Fit Nelson-Siegel model fc = FittedBondCurve(bonds, clean_prices, settlement, model="NS") assert fc.converged assert fc.rmse < 1.0 # good fit quality # Query the fitted curve df = fc.discount_factor(datetime.date(2025, 1, 15)) assert 0.9 < df < 1.0 # reasonable 1Y discount factor zr = fc.zero_rate(settlement, datetime.date(2029, 1, 15)) assert 0.0 < zr < 0.10 # positive 5Y zero rate # Per-bond pricing errors errors = fc.pricing_errors() assert len(errors) == 5 # one per bond # Extract the fitted inner curve inner = fc.curve() assert isinstance(inner, NelsonSiegel) ``` Advanced Example [#advanced-example-3] Convergence diagnostics, per-bond pricing errors, and inner curve extraction: ```python import datetime from vade import FittedBondCurve, FixedRateBond bonds = [ FixedRateBond(effective=datetime.date(2024, 6, 15), termination="2y", fixed_rate=3.5, frequency="s", convention="act365f", currency="usd"), FixedRateBond(effective=datetime.date(2024, 6, 15), termination="3y", fixed_rate=3.75, frequency="s", convention="act365f", currency="usd"), FixedRateBond(effective=datetime.date(2024, 6, 15), termination="5y", fixed_rate=4.0, frequency="s", convention="act365f", currency="usd"), FixedRateBond(effective=datetime.date(2024, 6, 15), termination="7y", fixed_rate=4.25, frequency="s", convention="act365f", currency="usd"), FixedRateBond(effective=datetime.date(2024, 6, 15), termination="10y", fixed_rate=4.5, frequency="s", convention="act365f", currency="usd"), ] clean_prices = [99.5, 99.0, 98.5, 97.5, 96.0] fc = FittedBondCurve( bonds=bonds, clean_prices=clean_prices, settlement=datetime.date(2025, 6, 16), model="NS", ) # Check convergence diagnostics assert fc.converged assert fc.iterations < 100 assert fc.rmse < 1.0 # RMSE in price terms # pricing_errors() returns per-bond fitting errors errors = fc.pricing_errors() assert len(errors) == 5 for err in errors: assert abs(err) < 2.0 # each bond fits within 2 price points # .curve() extracts the underlying parametric curve inner_curve = fc.curve() df = inner_curve.discount_factor(datetime.date(2030, 6, 16)) assert 0.70 < float(df) < 1.0 ``` > **Tip:** Use `pricing_errors()` to identify bonds that fit poorly -- these may indicate data issues or bonds with special features (e.g., on-the-run premiums). Consider excluding or down-weighting them via the `weights` parameter. # Rates API Reference (/docs/api/rates) Complete API reference for interest rate analytics: curve construction, instrument pricing, and multi-curve calibration. *** Pages [#pages] * [Instruments](./instruments) -- IRS, FRA, ZCS, SBS, OIS, Deposit, IRFuture, CapFloor * [Curves](./curves) -- DiscountCurve, LineCurve, ForwardCurve, CompositeCurve, SpreadCurve, TurnOfYearCurve, IRImpliedCurve, FXImpliedCurve, CreditImpliedCurve, FittedBondCurve, NelsonSiegel, NelsonSiegelSvensson, SmithWilson * [Solver](./solver) -- Multi-curve calibration, bootstrap, Levenberg-Marquardt solver See Also [#see-also] * [Rates Guides](../../guides/rates/) -- Curve building, pricing, risk, and cap/floor guides # Rates Instruments (/docs/api/rates/instruments) Interest rate derivatives for curve calibration, pricing, and risk. All types are available via flat import: ```bash from vade import IRS, FRA, ZCS, SBS, OIS, Deposit, IRFuture, CapFloor ``` Or via product-path import: ```bash from vade.instruments.rates import IRS, FRA, ZCS, SBS, OIS, Deposit, IRFuture, CapFloor ``` See [Conventions](../../conventions) for all accepted string parameter values. **Contents:** [IRS](#irs) | [FRA](#fra) | [ZCS](#zcs) | [SBS](#sbs) | [OIS](#ois) | [Deposit](#deposit) | [IRFuture](#irfuture) | [CapFloor](#capfloor) *** IRS [#irs] Interest Rate Swap: fixed vs floating leg. *Python wrapper composing FixedLeg + FloatLeg.* See also: [Pricing Guide](../guides/rates/pricing) for rates instrument pricing workflows. **Alias:** `IRS = InterestRateSwap` Constructor [#constructor] ```bash IRS( *, spec=None, effective=None, termination=None, frequency="a", fixed_rate=0.0, notional=1_000_000.0, convention="act360", float_convention=None, fixing_method="rfr_payment_delay", spread=0.0, calendar=None, payment_lag=None, currency="USD", amortization=None, fixed_amortization=None, float_amortization=None, modifier="mf", stub="shortfront", disc_curve_id=None, forecast_curve_id=None, ) ``` Parameters [#parameters] | Name | Type | Default | Description | | -------------------- | ------------------------- | --------------------- | ----------------------------------------------------------------- | | `spec` | `str` or `None` | `None` | Instrument spec name (overrides individual params) | | `effective` | `datetime.date` or `None` | `None` | Start date of the swap | | `termination` | `date`, `str`, or `None` | `None` | End date or tenor string (e.g., `"5Y"`) | | `frequency` | `str` | `"a"` | Payment frequency for both legs | | `fixed_rate` | `float` | `0.0` | Fixed leg rate (percentage, e.g., `3.0` for 3%) | | `notional` | `float` | `1_000_000.0` | Notional amount | | `convention` | `str` | `"act360"` | Day count convention for the fixed leg | | `float_convention` | `str` or `None` | `None` | Day count convention for the float leg (defaults to `convention`) | | `fixing_method` | `str` | `"rfr_payment_delay"` | Rate fixing method | | `spread` | `float` | `0.0` | Spread on the floating leg (basis points) | | `calendar` | `str` or `None` | `None` | Named calendar (e.g., `"NYC"`, `"LDN"`) | | `payment_lag` | `int` or `None` | `None` | Payment lag in business days | | `currency` | `str` | `"USD"` | Currency code | | `amortization` | schedule or `None` | `None` | Amortization schedule for both legs | | `fixed_amortization` | schedule or `None` | `None` | Amortization schedule for fixed leg only | | `float_amortization` | schedule or `None` | `None` | Amortization schedule for float leg only | | `modifier` | `str` | `"mf"` | Business day adjustment rule | | `stub` | `str` | `"shortfront"` | Stub period preference | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | | `forecast_curve_id` | `str` or `None` | `None` | Forecast curve identifier for [Solver](solver#solver) lookup | See [Conventions](../conventions) for accepted values for `frequency`, `convention`, `fixing_method`, `modifier`, and `stub`. Methods [#methods] | Method | Returns | Description | | ------------------------------------ | ----------- | --------------------------- | | `.rate(curves, *, solver=None)` | `float` | Par rate against curves | | `.npv(curves, *, solver=None)` | `float` | Net present value | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | | `.spread(curves, *, solver=None)` | `float` | Par spread | The `curves` parameter accepts a [DiscountCurve](curves#discountcurve), [LineCurve](curves#linecurve), list, dict, or `None`. When using a [Solver](solver#solver), pass the solver and curves are resolved by `disc_curve_id`/`forecast_curve_id`. Example [#example] ```python import datetime from vade import IRS, DiscountCurve nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97, datetime.date(2026, 1, 1): 0.94} curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") irs = IRS( effective=datetime.date(2024, 1, 1), termination="1Y", frequency="a", fixed_rate=3.0, convention="act365f", ) irs.npv(curve) # 817.9264117309795 irs.rate(curve) # 3.084091921661261 ``` *** FRA [#fra] Forward Rate Agreement: single-period forward rate instrument. *Python wrapper composing a single FloatPeriod.* **Alias:** `FRA = ForwardRateAgreement` Constructor [#constructor-1] ```bash FRA( *, spec=None, effective=None, termination=None, notional=1_000_000.0, fixed_rate=0.0, convention="act360", fixing_method="rfr_payment_delay", calendar=None, disc_curve_id=None, forecast_curve_id=None, ) ``` Parameters [#parameters-1] | Name | Type | Default | Description | | ------------------- | ------------------------- | --------------------- | ------------------------------------------------------------ | | `spec` | `str` or `None` | `None` | Instrument spec name | | `effective` | `datetime.date` or `None` | `None` | Start date | | `termination` | `date`, `str`, or `None` | `None` | End date or tenor string (e.g., `"6M"`) | | `notional` | `float` | `1_000_000.0` | Notional amount | | `fixed_rate` | `float` | `0.0` | Fixed rate (percentage) | | `convention` | `str` | `"act360"` | Day count convention | | `fixing_method` | `str` | `"rfr_payment_delay"` | Rate fixing method | | `calendar` | `str` or `None` | `None` | Named calendar | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | | `forecast_curve_id` | `str` or `None` | `None` | Forecast curve identifier for [Solver](solver#solver) lookup | See [Conventions](../conventions) for accepted values for `convention` and `fixing_method`. Methods [#methods-1] | Method | Returns | Description | | ------------------------------------ | ----------- | --------------------------- | | `.rate(curves, *, solver=None)` | `float` | Par rate against curves | | `.npv(curves, *, solver=None)` | `float` | Net present value | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | | `.spread(curves, *, solver=None)` | `float` | Par spread | Example [#example-1] ```python import datetime from vade import FRA, DiscountCurve nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2024, 7, 1): 0.985, datetime.date(2025, 1, 1): 0.97} curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360") fra = FRA( effective=datetime.date(2024, 3, 1), termination="6M", fixed_rate=3.5, convention="act360", ) fra.rate(curve) # forward rate implied by the curve fra.npv(curve) # NPV of the FRA position ``` *** ZCS [#zcs] Zero Coupon Swap: zero-coupon fixed vs floating leg. *Python wrapper composing ZeroFixedLeg + ZeroFloatLeg.* **Alias:** `ZCS = ZeroCouponSwap` Constructor [#constructor-2] ```bash ZCS( *, spec=None, effective=None, termination=None, fixed_rate=0.0, notional=1_000_000.0, convention="act360", float_convention=None, fixing_method="rfr_payment_delay", spread=0.0, calendar=None, currency="USD", disc_curve_id=None, forecast_curve_id=None, ) ``` Parameters [#parameters-2] | Name | Type | Default | Description | | ------------------- | ------------------------- | --------------------- | ------------------------------------------------------------ | | `spec` | `str` or `None` | `None` | Instrument spec name | | `effective` | `datetime.date` or `None` | `None` | Start date | | `termination` | `date`, `str`, or `None` | `None` | End date or tenor string | | `fixed_rate` | `float` | `0.0` | Fixed leg rate (percentage) | | `notional` | `float` | `1_000_000.0` | Notional amount | | `convention` | `str` | `"act360"` | Day count convention for the fixed leg | | `float_convention` | `str` or `None` | `None` | Day count convention for the float leg | | `fixing_method` | `str` | `"rfr_payment_delay"` | Rate fixing method | | `spread` | `float` | `0.0` | Spread on the floating leg (basis points) | | `calendar` | `str` or `None` | `None` | Named calendar | | `currency` | `str` | `"USD"` | Currency code | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | | `forecast_curve_id` | `str` or `None` | `None` | Forecast curve identifier for [Solver](solver#solver) lookup | See [Conventions](../conventions) for accepted values for `convention` and `fixing_method`. Methods [#methods-2] | Method | Returns | Description | | ------------------------------------ | ----------- | --------------------------- | | `.rate(curves, *, solver=None)` | `float` | Par rate against curves | | `.npv(curves, *, solver=None)` | `float` | Net present value | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | | `.spread(curves, *, solver=None)` | `float` | Par spread | Example [#example-2] ```python import datetime from vade import ZCS, DiscountCurve nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97, datetime.date(2026, 1, 1): 0.94} curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") zcs = ZCS( effective=datetime.date(2024, 1, 1), termination="2Y", fixed_rate=3.0, convention="act365f", ) zcs.rate(curve) # par zero-coupon rate zcs.npv(curve) # NPV of the ZCS position ``` *** SBS [#sbs] Single Basis Swap: two floating legs with different frequencies. *Python wrapper composing two FloatLegs.* **Alias:** `SBS = SingleBasisSwap` Constructor [#constructor-3] ```bash SBS( *, spec=None, effective=None, termination=None, frequency="q", leg2_frequency="s", notional=1_000_000.0, spread=0.0, convention="act360", leg2_convention=None, fixing_method="rfr_payment_delay", leg2_fixing_method=None, calendar=None, payment_lag=None, currency="USD", modifier="mf", stub="shortfront", disc_curve_id=None, forecast_curve_id=None, ) ``` Parameters [#parameters-3] | Name | Type | Default | Description | | -------------------- | ------------------------- | --------------------- | ------------------------------------------------------------ | | `spec` | `str` or `None` | `None` | Instrument spec name | | `effective` | `datetime.date` or `None` | `None` | Start date | | `termination` | `date`, `str`, or `None` | `None` | End date or tenor string | | `frequency` | `str` | `"q"` | Leg 1 payment frequency | | `leg2_frequency` | `str` | `"s"` | Leg 2 payment frequency | | `notional` | `float` | `1_000_000.0` | Notional amount | | `spread` | `float` | `0.0` | Spread on leg 1 (basis points) | | `convention` | `str` | `"act360"` | Day count convention for leg 1 | | `leg2_convention` | `str` or `None` | `None` | Day count convention for leg 2 (defaults to `convention`) | | `fixing_method` | `str` | `"rfr_payment_delay"` | Rate fixing method for leg 1 | | `leg2_fixing_method` | `str` or `None` | `None` | Rate fixing method for leg 2 (defaults to `fixing_method`) | | `calendar` | `str` or `None` | `None` | Named calendar | | `payment_lag` | `int` or `None` | `None` | Payment lag in business days | | `currency` | `str` | `"USD"` | Currency code | | `modifier` | `str` | `"mf"` | Business day adjustment rule | | `stub` | `str` | `"shortfront"` | Stub period preference | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | | `forecast_curve_id` | `str` or `None` | `None` | Forecast curve identifier for [Solver](solver#solver) lookup | See [Conventions](../conventions) for accepted values for `frequency`, `convention`, `fixing_method`, `modifier`, and `stub`. Methods [#methods-3] | Method | Returns | Description | | ------------------------------------ | ----------- | --------------------------- | | `.rate(curves, *, solver=None)` | `float` | Par basis spread | | `.npv(curves, *, solver=None)` | `float` | Net present value | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | | `.spread(curves, *, solver=None)` | `float` | Par spread | Example [#example-3] ```python import datetime from vade import SBS, DiscountCurve nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97, datetime.date(2026, 1, 1): 0.94} curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360") sbs = SBS( effective=datetime.date(2024, 1, 1), termination="2Y", frequency="q", leg2_frequency="s", convention="act360", ) sbs.rate(curve) # par basis spread between the two tenors sbs.npv(curve) # NPV of the basis swap ``` *** OIS [#ois] Overnight Indexed Swap: fixed vs RFR floating leg with compounding. *Python wrapper composing FixedLeg + FloatLeg with RFR compounding.* **Alias:** `OIS = OvernightIndexedSwap` Constructor [#constructor-4] ```bash OIS( *, spec=None, effective=None, termination=None, frequency="a", fixed_rate=0.0, notional=1_000_000.0, convention="act360", float_convention=None, fixing_method="rfr_payment_delay", spread=0.0, calendar=None, payment_lag=None, currency="USD", amortization=None, fixed_amortization=None, float_amortization=None, compounding_method=None, lockout=None, lookback=None, modifier="mf", stub="shortfront", disc_curve_id=None, forecast_curve_id=None, ) ``` Parameters [#parameters-4] | Name | Type | Default | Description | | -------------------- | ------------------------- | --------------------- | ------------------------------------------------------------ | | `spec` | `str` or `None` | `None` | Instrument spec name | | `effective` | `datetime.date` or `None` | `None` | Start date | | `termination` | `date`, `str`, or `None` | `None` | End date or tenor string | | `frequency` | `str` | `"a"` | Payment frequency | | `fixed_rate` | `float` | `0.0` | Fixed leg rate (percentage) | | `notional` | `float` | `1_000_000.0` | Notional amount | | `convention` | `str` | `"act360"` | Day count convention for the fixed leg | | `float_convention` | `str` or `None` | `None` | Day count convention for the float leg | | `fixing_method` | `str` | `"rfr_payment_delay"` | Rate fixing method | | `spread` | `float` | `0.0` | Spread on the floating leg (basis points) | | `calendar` | `str` or `None` | `None` | Named calendar | | `payment_lag` | `int` or `None` | `None` | Payment lag in business days | | `currency` | `str` | `"USD"` | Currency code | | `amortization` | schedule or `None` | `None` | Amortization schedule for both legs | | `fixed_amortization` | schedule or `None` | `None` | Amortization schedule for fixed leg only | | `float_amortization` | schedule or `None` | `None` | Amortization schedule for float leg only | | `compounding_method` | `str` or `None` | `None` | RFR compounding method (e.g., `"rfr_obs_shift"`) | | `lockout` | `int` or `None` | `None` | Number of lockout days before period end | | `lookback` | `int` or `None` | `None` | Number of lookback days for rate observation | | `modifier` | `str` | `"mf"` | Business day adjustment rule | | `stub` | `str` | `"shortfront"` | Stub period preference | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | | `forecast_curve_id` | `str` or `None` | `None` | Forecast curve identifier for [Solver](solver#solver) lookup | See [Conventions](../conventions) for accepted values for `frequency`, `convention`, `fixing_method`, `modifier`, and `stub`. Methods [#methods-4] | Method | Returns | Description | | ------------------------------------ | ----------- | --------------------------- | | `.rate(curves, *, solver=None)` | `float` | Par rate against curves | | `.npv(curves, *, solver=None)` | `float` | Net present value | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | | `.spread(curves, *, solver=None)` | `float` | Par spread | Example [#example-4] ```python import datetime from vade import OIS, DiscountCurve nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97, datetime.date(2026, 1, 1): 0.94} curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") ois = OIS( effective=datetime.date(2024, 1, 1), termination="1Y", frequency="a", fixed_rate=3.0, convention="act365f", ) ois.rate(curve) # par OIS rate ois.npv(curve) # NPV of the OIS position ``` *** Deposit [#deposit] Deposit instrument for short-end curve calibration. *Rust-backed.* Constructor [#constructor-5] ```bash Deposit( *, effective=None, termination=None, rate=0.0, notional=1_000_000.0, convention="act360", disc_curve_id=None, forecast_curve_id=None, ) ``` Parameters [#parameters-5] | Name | Type | Default | Description | | ------------------- | ------------------------- | ------------- | ------------------------------------------------------------ | | `effective` | `datetime.date` or `None` | `None` | Start date | | `termination` | `date`, `str`, or `None` | `None` | End date or tenor string (e.g., `"3M"`) | | `rate` | `float` | `0.0` | Deposit rate (percentage) | | `notional` | `float` | `1_000_000.0` | Notional amount | | `convention` | `str` | `"act360"` | Day count convention | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | | `forecast_curve_id` | `str` or `None` | `None` | Forecast curve identifier for [Solver](solver#solver) lookup | See [Conventions](../conventions) for accepted values for `convention`. Methods [#methods-5] | Method | Returns | Description | | ------------------------------------ | ----------- | --------------------------- | | `.rate(curves, *, solver=None)` | `float` | Par rate against curves | | `.npv(curves, *, solver=None)` | `float` | Net present value | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | | `.spread(curves, *, solver=None)` | `float` | Par spread | Example [#example-5] ```python import datetime from vade import Deposit, DiscountCurve nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2024, 4, 1): 0.9925, datetime.date(2024, 7, 1): 0.985} curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360") dep = Deposit( effective=datetime.date(2024, 1, 1), termination="3M", rate=3.0, convention="act360", ) dep.rate(curve) # par deposit rate implied by the curve dep.npv(curve) # NPV of the deposit ``` *** IRFuture [#irfuture] STIR Futures instrument with convexity adjustment. *Rust-backed.* Constructor [#constructor-6] ```bash IRFuture( *, effective=None, termination=None, price=100.0, notional=1_000_000.0, convexity_adjustment=0.0, convention="act360", quote_convention="price", sigma=None, tick_size=None, notional_multiplier=None, contract_size=None, disc_curve_id=None, forecast_curve_id=None, ) ``` Parameters [#parameters-6] | Name | Type | Default | Description | | ---------------------- | ------------------------- | ------------- | ------------------------------------------------------------ | | `effective` | `datetime.date` or `None` | `None` | Contract start date | | `termination` | `date`, `str`, or `None` | `None` | Contract end date or tenor string | | `price` | `float` | `100.0` | Futures price (e.g., `96.5` implies \~3.5% rate) | | `notional` | `float` | `1_000_000.0` | Notional amount | | `convexity_adjustment` | `float` | `0.0` | Convexity adjustment in basis points | | `convention` | `str` | `"act360"` | Day count convention | | `quote_convention` | `str` | `"price"` | Quote convention (`"price"` or `"rate"`) | | `sigma` | `float` or `None` | `None` | Volatility for automatic convexity calculation | | `tick_size` | `float` or `None` | `None` | Minimum price increment | | `notional_multiplier` | `float` or `None` | `None` | Notional multiplier per tick | | `contract_size` | `float` or `None` | `None` | Contract size | | `disc_curve_id` | `str` or `None` | `None` | Discount curve identifier for [Solver](solver#solver) lookup | | `forecast_curve_id` | `str` or `None` | `None` | Forecast curve identifier for [Solver](solver#solver) lookup | See [Conventions](../conventions) for accepted values for `convention`. Methods [#methods-6] | Method | Returns | Description | | ------------------------------------ | ----------- | --------------------------- | | `.rate(curves, *, solver=None)` | `float` | Implied forward rate | | `.npv(curves, *, solver=None)` | `float` | Net present value | | `.cashflows(curves, *, solver=None)` | `DataFrame` | Period-level cashflow table | | `.spread(curves, *, solver=None)` | `float` | Par spread | Example [#example-6] ```python import datetime from vade import IRFuture, DiscountCurve nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2024, 7, 1): 0.985, datetime.date(2025, 1, 1): 0.97} curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360") irf = IRFuture( effective=datetime.date(2024, 6, 1), termination="3M", price=96.5, convention="act360", ) irf.rate(curve) # implied forward rate from the curve irf.npv(curve) # NPV based on price vs implied rate ``` *** CapFloor [#capfloor] Cap/Floor instrument with Black-76 or Bachelier pricing. *Python wrapper composing caplet/floorlet periods.* See also: [Cap & Floor Guide](../guides/rates/capfloor) for cap/floor pricing and vol surface usage. Constructor [#constructor-7] ```bash CapFloor( *, effective=None, termination=None, strike=0.0, vol=0.0, notional=1_000_000.0, frequency="q", cap_floor_type="cap", model="black76", convention="act360", currency="USD", ) ``` Parameters [#parameters-7] | Name | Type | Default | Description | | ---------------- | ------------------------- | ------------- | ------------------------------------------- | | `effective` | `datetime.date` or `None` | `None` | Start date | | `termination` | `date`, `str`, or `None` | `None` | End date or tenor string | | `strike` | `float` | `0.0` | Strike rate (percentage) | | `vol` | `float` | `0.0` | Volatility (decimal, e.g., `0.20` for 20%) | | `notional` | `float` | `1_000_000.0` | Notional amount | | `frequency` | `str` | `"q"` | Caplet/floorlet frequency | | `cap_floor_type` | `str` | `"cap"` | Type: `"cap"` or `"floor"` | | `model` | `str` | `"black76"` | Pricing model: `"black76"` or `"bachelier"` | | `convention` | `str` | `"act360"` | Day count convention | | `currency` | `str` | `"USD"` | Currency code | See [Conventions](../conventions) for accepted values for `frequency` and `convention`. Methods [#methods-7] | Method | Returns | Description | | ---------------------------------------- | ----------- | --------------------------------------------- | | `.npv(disc_curve, forecast_curve)` | `float` | Net present value using Black-76 or Bachelier | | `.cashflows(disc_curve, forecast_curve)` | `DataFrame` | Caplet/floorlet-level cashflow table | Note: CapFloor does **not** have `.rate()` or `solver` keyword arguments. Both `disc_curve` and `forecast_curve` are required positional parameters. Example [#example-7] ```python import datetime from vade import CapFloor, DiscountCurve nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97, datetime.date(2026, 1, 1): 0.94} disc = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") forecast = DiscountCurve(nodes, interpolation="log_linear", convention="act365f") cap = CapFloor( effective=datetime.date(2024, 1, 1), termination="1Y", strike=3.0, vol=0.20, frequency="q", cap_floor_type="cap", model="black76", convention="act360", ) cap.npv(disc, forecast) # NPV of the interest rate cap ``` *** *** See Also [#see-also] * [Rates Guides](../../guides/rates/) -- Curve building, pricing, risk, and cap/floor guides * [Curves API](curves) -- DiscountCurve, ForwardCurve, and parametric models * [Solver API](solver) -- Multi-curve calibration and bootstrap # Solver (/docs/api/rates/solver) Multi-curve calibration solver with Levenberg-Marquardt, Gauss-Newton, and gradient descent algorithms, plus iterative bootstrap. All types are available via flat import: ```bash from vade import Solver, SolverResult from vade.solver import bootstrap ``` See [Conventions](../conventions) for all accepted string parameter values. See also: [Calibration Guide](../guides/calibration) for multi-curve calibration walkthroughs. | [Bootstrap & Parametric Guide](../guides/rates/bootstrap-parametric.md) for bootstrap vs optimizer comparison. | [Risk Guide](../guides/rates/risk.md) for solver-based delta and gamma computation. **Contents:** [SolverResult](#solverresult) | [Solver](#solver) | [bootstrap](#bootstrap) SolverResult [#solverresult] Calibration result with convergence diagnostics and Jacobian. *Rust-backed.* Returned by [`Solver.iterate()`](#solver) -- not constructed directly. Properties [#properties] | Property | Type | Description | | ------------------- | --------------- | --------------------------------------------------------- | | `.converged` | `bool` | Whether calibration converged | | `.iterations` | `int` | Number of iterations performed | | `.objective` | `float` | Final objective function value (sum of squared residuals) | | `.residuals` | `list[float]` | Per-instrument residuals (target - computed) | | `.jacobian` | `numpy.ndarray` | 2D Jacobian matrix, shape (n\_instruments, n\_vars) | | `.condition_number` | `float` | Condition number of the Jacobian | | `.status` | `str` | Status message describing termination reason | | `.time_seconds` | `float` | Calibration wall-clock time in seconds | | `.algorithm` | `str` | Algorithm used (e.g., `"levenberg_marquardt"`) | Example [#example] ```python import datetime from vade import IRS, DiscountCurve, Solver nodes = { datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 1.0, datetime.date(2026, 1, 1): 1.0, } curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f", id="usd") irs_1y = IRS(effective=datetime.date(2024, 1, 1), termination="1Y", frequency="a", fixed_rate=3.0, convention="act365f", float_convention="act365f") irs_2y = IRS(effective=datetime.date(2024, 1, 1), termination="2Y", frequency="a", fixed_rate=3.5, convention="act365f", float_convention="act365f") solver = Solver(curves=[curve], instruments=[(irs_1y, 3.0), (irs_2y, 3.5)]) result = solver.iterate() result.converged # True result.iterations > 0 # True result.algorithm # 'levenberg_marquardt' result.objective < 1e-10 # True len(result.residuals) # 2 result.jacobian.shape # (2, 2) result.condition_number > 0 # True isinstance(result.time_seconds, float) # True isinstance(result.status, str) # True ``` Solver [#solver] Multi-curve calibration solver with selectable optimization algorithm. *Rust-backed.* Constructor [#constructor] ```bash Solver( *, curves, instruments, weights=None, func_tol=1e-14, grad_tol=1e-14, max_iter=100, ini_lambda=None, pre_solvers=None, fx_rates=None, algorithm="levenberg_marquardt", learning_rate=None, ) ``` Parameters [#parameters] | Name | Type | Default | Description | | --------------- | ------------------------------------ | ----------------------- | --------------------------------------------------------------------------------------- | | `curves` | `list[DiscountCurve \| LineCurve]` | *required* | Curves to calibrate (node values are adjusted) | | `instruments` | `list[tuple \| dict]` | *required* | Calibration instruments with targets (see [Instruments Format](#instruments-format)) | | `weights` | `list[float] \| None` | `None` | Per-instrument weights (uniform if None) | | `func_tol` | `float` | `1e-14` | Function tolerance for convergence | | `grad_tol` | `float` | `1e-14` | Gradient tolerance for convergence | | `max_iter` | `int` | `100` | Maximum iterations | | `ini_lambda` | `tuple[float, float, float] \| None` | `None` | LM damping parameters (initial, factor\_up, factor\_down) | | `pre_solvers` | `list[Solver] \| None` | `None` | Solvers to run first (staged calibration for multi-curve frameworks) | | `fx_rates` | `FXRates \| None` | `None` | FX rates for cross-currency calibration | | `algorithm` | `str` | `"levenberg_marquardt"` | Optimization algorithm: `"levenberg_marquardt"`, `"gauss_newton"`, `"gradient_descent"` | | `learning_rate` | `float \| None` | `None` | Learning rate for gradient descent (default 1.0 with Armijo backtracking) | See [DiscountCurve](curves#discountcurve), [LineCurve](curves#linecurve) for curves. See [FXRates](fx#fxrates) for cross-currency calibration. Methods [#methods] | Method | Returns | Description | | ----------------------------- | -------------- | ---------------------------------------------------------------------------------- | | `.iterate()` | `SolverResult` | Run calibration, returns result with convergence info | | `.get_curve(index=0)` | `Curve` | Get calibrated curve by index | | `.get_curve_by_id(curve_id)` | `Curve` | Get calibrated curve by its id string | | `.delta(instrument, result)` | `DataFrame` | Bucket-level delta risk (columns: instrument\_label, tenor, sensitivity, currency) | | `.gamma(instrument, result)` | `DataFrame` | Cross-gamma matrix as labeled DataFrame | | `.to_json()` | `str` | Serialize solver state to JSON | | `.from_json(s)` (classmethod) | `Solver` | Deserialize from JSON | Instruments Format [#instruments-format] The `instruments` parameter accepts three formats: **Simple 2-tuple** -- single-curve calibration: ```bash instruments = [(irs_1y, 3.50), (irs_2y, 3.75), (irs_5y, 4.00)] ``` **Extended 5-tuple** -- multi-curve with labels (required for `.delta()` and `.gamma()`): ```bash instruments = [ (irs_1y, 3.50, 0, "1Y_IRS", "USD"), (irs_2y, 3.75, 0, "2Y_IRS", "USD"), ] ``` Fields: `(instrument, target, curve_idx, label, currency)`. **Dict format** -- multi-curve with explicit disc/forecast curve indices: ```bash instruments = [ { "instrument": irs, "target": 3.50, "disc_curve_idx": 0, "forecast_curve_idx": 1, "label": "1Y", "currency": "USD", } ] ``` Keys: `instrument` (required), `target` (required), `disc_curve_idx` (required), `forecast_curve_idx` (optional, defaults to disc\_curve\_idx), `label` (optional), `currency` (optional). The dict format also supports `leg2_disc_curve_idx` and `leg2_forecast_curve_idx` for cross-currency swaps with four curves. Example [#example-1] ```python import datetime from vade import IRS, DiscountCurve, Solver nodes = { datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 1.0, datetime.date(2026, 1, 1): 1.0, datetime.date(2027, 1, 1): 1.0, } curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f", id="usd") irs_1y = IRS(effective=datetime.date(2024, 1, 1), termination="1Y", frequency="a", fixed_rate=3.0, convention="act365f", float_convention="act365f") irs_2y = IRS(effective=datetime.date(2024, 1, 1), termination="2Y", frequency="a", fixed_rate=3.2, convention="act365f", float_convention="act365f") irs_3y = IRS(effective=datetime.date(2024, 1, 1), termination="3Y", frequency="a", fixed_rate=3.4, convention="act365f", float_convention="act365f") solver = Solver(curves=[curve], instruments=[(irs_1y, 3.0), (irs_2y, 3.2), (irs_3y, 3.4)]) result = solver.iterate() result.converged # True calibrated = solver.get_curve(0) df_1y = calibrated.discount_factor(datetime.date(2025, 1, 1)) df_1y > 0.95 # True df_1y < 1.0 # True ``` bootstrap [#bootstrap] Iterative bootstrap calibration using node-by-node Brent root-finding. Module-level function. ```bash from vade.solver import bootstrap result = bootstrap( curves, instruments, target_curve_idx, tol=None, max_outer=None, max_brent=None, bracket_low=None, bracket_high=None, ) ``` Parameters [#parameters-1] | Name | Type | Default | Description | | ------------------ | --------------- | ---------- | --------------------------------------------------------------- | | `curves` | `list[Curve]` | *required* | Curves to calibrate | | `instruments` | `list[dict]` | *required* | Instruments in dict format (see keys below) | | `target_curve_idx` | `int` | *required* | Index of curve to calibrate in curves list | | `tol` | `float \| None` | `None` | Root-finding tolerance (default: 1e-12) | | `max_outer` | `int \| None` | `None` | Maximum outer iterations for global interpolators (default: 10) | | `max_brent` | `int \| None` | `None` | Maximum Brent iterations per node (default: 100) | | `bracket_low` | `float \| None` | `None` | Lower bracket for Brent search (default: -0.05) | | `bracket_high` | `float \| None` | `None` | Upper bracket for Brent search (default: 0.30) | The `instruments` list uses dict format with keys: | Key | Type | Required | Description | | -------------------- | ------- | -------- | ------------------------------------------------------ | | `instrument` | object | yes | Instrument's inner Rust binding (e.g., `irs._inner`) | | `target` | `float` | yes | Target rate to calibrate to | | `disc_curve_idx` | `int` | yes | Index of discount curve in curves list | | `forecast_curve_idx` | `int` | no | Index of forecast curve (defaults to disc\_curve\_idx) | | `label` | `str` | no | Instrument label for diagnostics | **Returns:** calibrated curve (same type as `curves[target_curve_idx]`). **Note:** The number of instruments must equal the number of calibratable nodes on the target curve (all nodes except the anchor node at the curve's start date). Each instrument calibrates one node, ordered by maturity. **Note:** Unlike [`Solver`](#solver), bootstrap requires the instrument's internal Rust binding via `._inner`. The Solver wrapper handles this conversion automatically, but bootstrap operates at the lower level. **When to use bootstrap() vs Solver:** `bootstrap()` performs node-by-node Brent root-finding -- fast and exact for sequential calibration where each instrument maps to one curve node. Use [`Solver`](#solver) when instruments share nodes, when you need Levenberg-Marquardt optimization, or when calibrating multiple curves simultaneously. Example [#example-2] ```python import datetime from vade import DiscountCurve, Deposit, IRS from vade.solver import bootstrap effective = datetime.date(2025, 6, 16) # Curve with one anchor node (effective=1.0) plus one node per instrument curve = DiscountCurve( { effective: 1.0, datetime.date(2025, 12, 16): 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, }, interpolation="log_linear", convention="act360", ) # Instruments with rate=0.0 / fixed_rate=0.0 (targets come from instruments dict) dep_6m = Deposit(effective=effective, termination="6m", rate=0.0, convention="act360") irs_1y = IRS(effective=effective, termination="1y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_2y = IRS(effective=effective, termination="2y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") # Use ._inner for Rust-level instrument bindings # bracket_low/bracket_high set for DF-space search (not rate-space) calibrated = bootstrap( curves=[curve], instruments=[ {"instrument": dep_6m._inner, "target": 0.0425, "disc_curve_idx": 0, "label": "6M Depo"}, {"instrument": irs_1y._inner, "target": 4.00, "disc_curve_idx": 0, "label": "1Y IRS"}, {"instrument": irs_2y._inner, "target": 3.85, "disc_curve_idx": 0, "label": "2Y IRS"}, ], target_curve_idx=0, bracket_low=0.5, bracket_high=1.5, ) # Verify calibration: discount factors decrease with maturity df_6m = calibrated.discount_factor(datetime.date(2025, 12, 16)) df_1y = calibrated.discount_factor(datetime.date(2026, 6, 16)) df_2y = calibrated.discount_factor(datetime.date(2027, 6, 16)) assert 0.95 < float(df_6m) < 1.0 assert 0.90 < float(df_1y) < 1.0 assert 0.85 < float(df_2y) < 0.98 assert float(df_6m) > float(df_1y) > float(df_2y) # Repricing verification: calibrated curve reproduces target rates repriced_6m = float(dep_6m.rate(calibrated)) assert abs(repriced_6m - 0.0425) < 1e-6 ``` > **See also:** [Bootstrap vs Parametric Calibration](../guides/rates/bootstrap-parametric) for a full walkthrough comparing bootstrap and Solver approaches. For most calibration workflows, prefer [`Solver`](#solver) which provides richer diagnostics (convergence info, Jacobian, delta/gamma risk). Bootstrap is useful when you need node-by-node stripping with local interpolators (e.g., `"flat_forward"`). # Bonds (/docs/guides/credit/bonds) Compute settlement-aware analytics for a [FixedRateBond](../../api/credit/instruments#fixedratebond) including clean and dirty prices, yield to maturity, duration, convexity, and spread measures. A single bond instrument is threaded through all sections. *** Setup [#setup] Create a 10-year semi-annual coupon bond and calibrate a discount curve from Treasury-like market data. The bond uses the [actactisda](../../conventions#day-count-conventions) day count convention, standard for government bonds. ```python import datetime from vade import FixedRateBond, DiscountCurve, Deposit, IRS, Solver effective = datetime.date(2025, 1, 15) settlement = datetime.date(2025, 6, 16) bond = FixedRateBond( effective=effective, termination="10y", coupon=0.05, frequency="s", settlement_days=1, convention="actactisda", face_value=100.0, ) curve = DiscountCurve( { settlement: 1.0, datetime.date(2025, 9, 16): 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, datetime.date(2032, 6, 16): 1.0, datetime.date(2035, 1, 15): 1.0, }, interpolation="log_linear", convention="act360", id="tsy", ) dep = Deposit(effective=settlement, termination="3m", rate=0.0, convention="act360") irs_1y = IRS(effective=settlement, termination="1y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_2y = IRS(effective=settlement, termination="2y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_3y = IRS(effective=settlement, termination="3y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_5y = IRS(effective=settlement, termination="5y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_7y = IRS(effective=settlement, termination="7y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_10y = IRS(effective=settlement, termination="10y", frequency="s", fixed_rate=0.0, convention="act360", float_convention="act360") instruments = [ (dep, 0.0440), (irs_1y, 4.20), (irs_2y, 4.10), (irs_3y, 4.00), (irs_5y, 3.90), (irs_7y, 3.95), (irs_10y, 4.05), ] solver = Solver(curves=[curve], instruments=instruments) result = solver.iterate() assert result.converged calibrated = solver.get_curve(0) ``` Clean and Dirty Price [#clean-and-dirty-price] The clean price excludes accrued interest and is the standard market quotation. The dirty price (also called full price) is what the buyer actually pays: clean price plus accrued interest from the last coupon date to settlement. ```python continuation clean = float(bond.price(calibrated, settlement=settlement)) dirty = float(bond.dirty_price(calibrated, settlement=settlement)) accrued = float(bond.accrued_interest(settlement)) clean # 105.84104013229583 dirty # 107.94048764610798 accrued # 2.0994475138121547 abs(dirty - (clean + accrued)) < 1e-10 # True ``` Yield to Maturity [#yield-to-maturity] Yield to maturity (YTM) is the single discount rate that equates the bond's future cashflows to its current price. The `ytm` method solves for yield from a clean price, and `price_from_ytm` inverts back to the dirty price for round-trip verification. ```python continuation ytm = float(bond.ytm(clean_price=clean, settlement=settlement)) ytm # 0.04250919245943899 dirty_from_ytm = float(bond.price_from_ytm(ytm, settlement=settlement)) dirty_from_ytm # 107.94048764610811 abs(dirty_from_ytm - dirty) < 1e-6 # True ``` Duration and Convexity [#duration-and-convexity] Modified duration measures the bond's price sensitivity to a parallel shift in yields -- a modified duration of 7.5 means a 1% rate rise causes roughly a 7.5% price decline. Macaulay duration is the weighted-average time to receive cashflows. Convexity captures the curvature of the price-yield relationship, and DV01 is the dollar value of a one basis point rate move. ```python continuation float(bond.duration(calibrated, metric="modified", settlement=settlement)) # 7.47939995668882 float(bond.duration(calibrated, metric="macaulay", settlement=settlement)) # 7.638371582808822 float(bond.convexity(calibrated, settlement=settlement)) # 68.48920229165095 float(bond.dv01(calibrated)) # 0.08547801519775078 ``` Z-Spread and Asset Swap Spread [#z-spread-and-asset-swap-spread] The Z-spread is the constant spread added to every point on the discount curve that reprices the bond to its market price -- a Z-spread near zero means the bond is fairly valued against the curve. The asset swap spread (ASW) measures the bond's spread over the floating rate in a par-par asset swap package. ```python continuation z_spread = float(bond.z_spread(calibrated, price=clean, settlement=settlement)) z_spread # 7.716154286450026e-18 asw = float(bond.asw_spread(calibrated, price=clean, method="par_par", settlement=settlement)) asw # 0.0025332873590058385 ``` Zero-Coupon Bonds [#zero-coupon-bonds] Zero-coupon bonds pay no coupons and are issued at a discount to face value. The return comes entirely from the price appreciation to par at maturity. Accrued interest uses the OID (original issue discount) constant-yield method, where the bond accretes toward face value over its life. See [ZeroCouponBond](../../api/credit/instruments#zerocouponbond) API reference. ```python continuation zcb = ZeroCouponBond( effective=effective, termination="5y", issue_price=90.0, convention="actactisda", face_value=100.0, ) zcb_price = float(zcb.price(calibrated, settlement=settlement)) zcb_dirty = float(zcb.dirty_price(calibrated, settlement=settlement)) zcb_ytm = float(zcb.ytm(zcb_price, settlement)) zcb_dur = float(zcb.duration(calibrated, metric="modified", settlement=settlement)) assert 0 < zcb_price < 200 assert zcb_dirty > 0 assert 0 < zcb_ytm < 0.20 assert zcb_dur > 0 ``` Step-Up Bonds [#step-up-bonds] Step-up bonds have coupon rates that change over the bond's life according to a predetermined schedule. They are common in corporate bonds and agency securities where the issuer agrees to pay higher coupons in later years. See [StepUpBond](../../api/credit/instruments#stepupbond) API reference. ```python continuation step = StepUpBond( effective=effective, termination="4y", coupon_rates=[ (effective, 0.03), (datetime.date(2027, 6, 16), 0.045), ], frequency="a", face_value=100.0, ) step_price = float(step.price(calibrated, settlement=settlement)) step_accrued = float(step.accrued_interest(settlement)) step_ytm = float(step.ytm(step_price, settlement)) step_dur = float(step.duration(calibrated, metric="modified", settlement=settlement)) assert 0 < step_price < 200 assert step_accrued >= 0 assert 0 < step_ytm < 0.20 assert step_dur > 0 ``` Amortizing Bonds [#amortizing-bonds] Amortizing bonds repay principal in installments rather than in a single bullet at maturity. This reduces the outstanding notional over time, lowering both duration and credit exposure. The amortization schedule specifies repayment amounts at specific coupon dates. See [AmortizingBond](../../api/credit/instruments#amortizingbond) API reference. ```python continuation amort = AmortizingBond( effective=effective, termination="3y", coupon=0.04, amortization_schedule=[ (datetime.date(2026, 1, 15), 30.0), (datetime.date(2027, 1, 15), 30.0), ], frequency="a", face_value=100.0, convention="actactisda", ) amort_price = float(amort.price(calibrated, settlement=settlement)) amort_dur = float(amort.duration(calibrated, metric="modified", settlement=settlement)) assert 0 < amort_price < 200 assert amort_dur > 0 # Cashflows show both coupon and principal repayment rows cfs = amort.cashflows(calibrated) period_types = cfs["Type"].to_list() assert "Amortizing" in period_types ``` PIK Bonds [#pik-bonds] Payment-in-kind (PIK) bonds capitalize accrued interest into the outstanding notional instead of paying cash coupons. The notional grows over time as interest compounds. A `pik_fraction` of 1.0 means full PIK (no cash coupons); values between 0 and 1 create partial PIK bonds that pay some cash and capitalize the rest. See [PIKBond](../../api/credit/instruments#pikbond) API reference. ```python continuation pik = PIKBond( effective=effective, termination="3y", coupon=0.06, pik_fraction=1.0, frequency="a", face_value=100.0, convention="actactisda", ) # Notional schedule shows how notional grows each period schedule = pik.notional_schedule() assert len(schedule) >= 3 assert schedule[0][1] == 100.0 # starts at face value assert schedule[1][1] > 100.0 # grows after first period assert schedule[-1][1] > schedule[0][1] # keeps growing pik_ytm = float(pik.ytm(100.0, effective)) assert 0 < pik_ytm < 0.20 ``` Capped and Floored Floating-Rate Notes [#capped-and-floored-floating-rate-notes] A capped floating-rate note limits the maximum coupon rate via an embedded cap option, while a floor sets a minimum. When both are present the structure is called a collar. Cap and floor options are priced using Black-76, so a volatility parameter (`vol`) is always required. See [CappedFloatRateNote](../../api/credit/instruments#cappedfloatratenote) API reference. ```python continuation capped = CappedFloatRateNote( effective=effective, termination="2y", spread=0.005, frequency="q", cap_rate=0.06, floor_rate=0.01, vol=0.20, convention="act360", ) capped_dp = float(capped.dirty_price(curves=[calibrated, calibrated])) assert 80.0 < capped_dp < 120.0 ``` Next Steps [#next-steps] * [Spread Analytics](spread-analytics) -- I-spread and asset swap analysis * [Callable Bonds](callable-bonds) -- Callable bond pricing with OAS * [Fitted Curves](fitted-curves) -- Parametric curve fitting to bond portfolios * [Credit Curves & CDS](credit-curves-cds) -- Credit curve bootstrapping and CDS pricing * [Serialization](../serialization) -- Save and restore curves via JSON # Callable Bonds (/docs/guides/credit/callable-bonds) Price callable bonds using a Hull-White one-factor short-rate model with trinomial tree pricing. Compute option-adjusted spreads (OAS) and tree-based risk measures (effective duration, effective convexity, vega). *** Setup [#setup] Create a 5-year annual coupon bond with a call schedule allowing the issuer to redeem at par starting in year 2. We build a flat discount curve using continuous compounding for the tree pricer. ```python import datetime import math from vade import FixedRateBond, CallableBond, DiscountCurve effective = datetime.date(2024, 1, 1) # Flat discount curve (3% continuously compounded) nodes = {effective: 1.0} for y in range(1, 11): d = datetime.date(2024 + y, 1, 1) nodes[d] = math.exp(-0.03 * y) curve = DiscountCurve( nodes, interpolation="log_linear", convention="act365f", id="hw_curve", ) # Underlying non-callable bond bond = FixedRateBond( effective=effective, termination="5y", coupon=0.04, frequency="a", face_value=100.0, ) # Callable bond: issuer can call at par from year 2 onward cb = CallableBond( bond=bond, call_schedule=[ (datetime.date(2026, 1, 1), 100.0), (datetime.date(2027, 1, 1), 100.0), (datetime.date(2028, 1, 1), 100.0), ], ) assert repr(cb) == "CallableBond(...)" ``` Tree Pricing [#tree-pricing] The Hull-White trinomial tree discretizes the short-rate process and values the bond backward from maturity, accounting for call exercise at each node. Parameter `a` is the mean reversion speed and `sigma` is the short-rate volatility. See [CallableBond](../../api/credit/instruments#callablebond) API reference. ```python continuation callable_price = cb.tree_price(curves=curve, a=0.1, sigma=0.01) assert math.isfinite(callable_price) assert 50.0 < callable_price < 150.0 # The callable price should be at most the non-callable price, # since the call option benefits the issuer (not the holder) noncallable_price = float(bond.dirty_price(curve)) assert math.isfinite(noncallable_price) ``` Option-Adjusted Spread [#option-adjusted-spread] OAS is the constant spread added to the tree's forward rates that equates the model price to the observed market price. It measures the bond's yield premium after removing the embedded call option cost. A positive OAS means the bond trades cheap relative to the model. ```python continuation # Use a market price slightly below model price (bond trades cheap) market_price = callable_price - 0.5 oas_val = cb.oas(curves=curve, market_price=market_price, a=0.1, sigma=0.01) assert math.isfinite(oas_val) assert oas_val > 0 # cheaper market price => positive OAS # When market_price equals model price, OAS should be ~0 oas_zero = cb.oas(curves=curve, market_price=callable_price, a=0.1, sigma=0.01) assert abs(oas_zero) < 1e-4 ``` Effective Duration and Convexity [#effective-duration-and-convexity] For callable bonds, modified duration is not meaningful because future cashflows depend on whether the issuer exercises the call. Effective duration uses finite-difference bump-and-reprice on the tree to capture the impact of optionality on rate sensitivity. ```python continuation eff_dur = cb.effective_duration(curves=curve, a=0.1, sigma=0.01) eff_cvx = cb.effective_convexity(curves=curve, a=0.1, sigma=0.01) assert math.isfinite(eff_dur) assert eff_dur > 0 assert math.isfinite(eff_cvx) ``` Vega [#vega] Vega measures the sensitivity of the callable bond price to changes in short-rate volatility. A higher vol increases the value of the embedded call option, reducing the callable bond price for the holder. ```python continuation v = cb.vega(curves=curve, a=0.1, sigma=0.01) assert math.isfinite(v) ``` Next Steps [#next-steps] * [Spread Analytics](spread-analytics) -- I-spread and asset swap analysis * [Fitted Curves](fitted-curves) -- Parametric curve fitting * [Bonds](bonds) -- Core bond analytics # Credit Curves & CDS (/docs/guides/credit/credit-curves-cds) Model default risk with [CreditImpliedCurve](../../api/rates/curves#creditimpliedcurve) using piecewise flat hazard rates, and price credit protection with [CDS](../../api/credit/instruments#cds) instruments against risk-free discount and credit curves. *** Credit Curves [#credit-curves] A [CreditImpliedCurve](../../api/rates/curves#creditimpliedcurve) models default risk through piecewise flat hazard rates. Nodes map dates to hazard rate levels, and the curve computes survival probabilities -- the likelihood that the reference entity has not defaulted by a given date. The [act360](../../conventions#day-count-conventions) convention is standard for credit markets. ```python credit_curve.survival_probability(datetime.date(2027, 6, 16)) # 0.9779413739743786 credit_curve.survival_probability(datetime.date(2030, 6, 16)) # 0.9371403492367462 ``` CDS Pricing [#cds-pricing] A [CDS](../../api/credit/instruments#cds) (Credit Default Swap) provides protection against default. The buyer pays a quarterly fixed premium (in basis points) and receives a payout if the reference entity defaults. Pricing requires two curves: a risk-free discount curve and a credit curve. Note that `fixed_rate` is in basis points -- 100.0 means 100bp (1%). ```python continuation cds = CDS( effective=effective, termination="5y", frequency="q", fixed_rate=100.0, recovery_rate=0.4, convention="act360", notional=10_000_000.0, ) float(cds.npv(disc_curve, credit_curve)) # -105585.47418774274 float(cds.rate(disc_curve, credit_curve)) # 76.38869933298339 float(cds.spread(disc_curve, credit_curve)) # -23.611300667016607 ``` Next Steps [#next-steps] * [Bonds](bonds) -- Settlement-aware bond analytics, duration, convexity, and spread measures * [Spread Analytics](spread-analytics) -- Interpolated swap spreads (I-spread) and asset swap analysis # Fitted Bond Curves (/docs/guides/credit/fitted-curves) Fit parametric yield curves to a portfolio of bond market prices using nonlinear least-squares optimization. Supports Nelson-Siegel (NS), Nelson-Siegel-Svensson (NSS), and Smith-Wilson (SW) models. *** Setup [#setup] Create a portfolio of fixed-rate bonds with different maturities and their observed market prices. The fitted curve will find the parametric yield curve that best reprices these bonds. ```python import datetime from vade import FixedRateBond, FittedBondCurve settlement = datetime.date(2024, 1, 15) bonds = [ FixedRateBond( effective=settlement, termination=datetime.date(2026, 1, 15), frequency="s", convention="act365f", coupon=0.03, face_value=100.0, ), FixedRateBond( effective=settlement, termination=datetime.date(2027, 1, 15), frequency="s", convention="act365f", coupon=0.035, face_value=100.0, ), FixedRateBond( effective=settlement, termination=datetime.date(2029, 1, 15), frequency="s", convention="act365f", coupon=0.04, face_value=100.0, ), FixedRateBond( effective=settlement, termination=datetime.date(2031, 1, 15), frequency="s", convention="act365f", coupon=0.042, face_value=100.0, ), FixedRateBond( effective=settlement, termination=datetime.date(2034, 1, 15), frequency="s", convention="act365f", coupon=0.045, face_value=100.0, ), ] clean_prices = [99.5, 99.0, 98.5, 97.0, 95.0] ``` Nelson-Siegel [#nelson-siegel] The Nelson-Siegel model parametrizes the zero curve with four parameters (beta0, beta1, beta2, tau) capturing the level, slope, and curvature of the term structure. It is widely used by central banks for yield curve estimation. See [FittedBondCurve](../../api/rates/curves#fittedbondcurve) API reference. ```python continuation fc_ns = FittedBondCurve(bonds, clean_prices, settlement, "NS") assert fc_ns.converged assert fc_ns.rmse < 1.0 # Query the fitted curve df = fc_ns.discount_factor(datetime.date(2029, 1, 15)) assert 0.5 < df < 1.0 zr = fc_ns.zero_rate(settlement, datetime.date(2029, 1, 15)) assert 0.0 < zr < 0.10 ``` Nelson-Siegel-Svensson [#nelson-siegel-svensson] NSS extends Nelson-Siegel with two additional parameters (beta3, tau2) that add a second hump to the forward rate curve. This provides a better fit to term structures with complex shapes, such as those with a dip at medium maturities. ```python continuation fc_nss = FittedBondCurve(bonds, clean_prices, settlement, "NSS") assert fc_nss.converged assert fc_nss.rmse < 1.0 ``` Smith-Wilson [#smith-wilson] The Smith-Wilson method extrapolates the fitted curve toward an ultimate forward rate (UFR), which is required for regulatory discount curves under Solvency II. Parameters `ufr` and `alpha` control the target long-term rate and the speed of convergence. ```python continuation fc_sw = FittedBondCurve(bonds, clean_prices, settlement, "SW", ufr=0.042, alpha=0.1) assert fc_sw.converged assert fc_sw.rmse < 1.0 ``` Custom Initial Parameters [#custom-initial-parameters] Pass `initial_params` to override the default starting point for the optimizer. This is useful when the default guess fails to converge or converges to a local minimum. The number of elements must match the model: four for NS (beta0, beta1, beta2, tau) and six for NSS (beta0, beta1, beta2, beta3, tau1, tau2). ```python continuation fc_custom = FittedBondCurve( bonds, clean_prices, settlement, "NSS", initial_params=[0.04, -0.01, 0.005, 0.005, 1.0, 5.0], ) assert fc_custom.converged assert fc_custom.rmse < 1.0 ``` Fit Quality [#fit-quality] After fitting, inspect per-bond pricing errors and RMSE to assess fit quality. Large errors on specific bonds may indicate outliers or model limitations. The `pricing_errors()` method returns the signed difference between model and market prices for each bond. ```python continuation errors = fc_ns.pricing_errors() assert len(errors) == len(bonds) # All errors should be small relative to price for err in errors: assert abs(err) < 2.0 # within 2 price points # RMSE summarises overall fit quality assert isinstance(fc_ns.rmse, float) assert fc_ns.rmse >= 0.0 assert fc_ns.iterations > 0 ``` Next Steps [#next-steps] * [Callable Bonds](callable-bonds) -- OAS using fitted curves as input * [Bootstrap & Parametric](../rates/bootstrap-parametric) -- Alternative parametric curve construction * [Bonds](bonds) -- Core bond analytics # Credit (/docs/guides/credit) Vade's credit analytics cover bond pricing and risk, credit default swaps, credit curve construction, and spread analysis. Compute settlement-aware bond measures, price CDS contracts, fit parametric curves to bond portfolios, and decompose yield into spread components. *** What's Covered [#whats-covered] * **Bonds** -- FixedRateBond with clean/dirty price, yield to maturity, duration, convexity, and Z-spread * **Bond variants** -- CallableBond (Hull-White tree pricing with OAS), ZeroCouponBond, Bill, AmortizingBond, StepUpBond, PIKBond, CappedFloatRateNote, FloatRateNote * **Credit derivatives** -- CDS (credit default swap) pricing and risk * **Credit curves** -- CreditImpliedCurve for hazard rate bootstrapping from CDS spreads * **Fitted curves** -- FittedBondCurve with NelsonSiegel, NelsonSiegelSvensson, and SmithWilson models * **Spread analysis** -- SpreadCurve, AssetSwap, I-spread, Z-spread, OAS decomposition Key Concepts [#key-concepts] * **Credit spread** -- the yield premium a bond pays over the risk-free curve, compensating for default risk * **Hazard rate** -- the instantaneous conditional probability of default, implied from CDS market spreads * **Recovery rate** -- the fraction of notional recovered after a credit event, typically 40% for senior unsecured * **Z-spread** -- the constant spread added to the risk-free curve that reprices a bond to its market price * **OAS** -- option-adjusted spread; the Z-spread net of any embedded optionality (call, put) * **Duration** -- the sensitivity of a bond's price to a parallel shift in yield (modified or Macaulay) * **Convexity** -- the second-order sensitivity of price to yield; measures curvature of the price-yield relationship * **Asset swap spread** -- the spread over the floating index that equates a bond's cashflows to par in a swap structure Quick Taste [#quick-taste] ```python bond = FixedRateBond(effective=effective, termination="5y", coupon=0.045, frequency="s", convention="actactisda") dirty = float(bond.dirty_price(calibrated, settlement=effective)) # A bond priced above par has a coupon exceeding the prevailing yield assert dirty > 100.0 ``` Guides [#guides] * [Credit Curves & CDS](./credit-curves-cds) -- Credit curve bootstrapping and CDS pricing * [Bonds](./bonds) -- Settlement-aware bond analytics, duration, convexity, and spread measures * [Callable Bonds](./callable-bonds) -- Hull-White trinomial tree pricing with OAS and effective risk measures * [Fitted Curves](./fitted-curves) -- Parametric curve fitting to bond portfolios via nonlinear least-squares * [Spread Analytics](./spread-analytics) -- Interpolated swap spreads (I-spread) and asset swap analysis API Reference [#api-reference] * [Curves](../../api/rates/curves) -- CreditImpliedCurve, FittedBondCurve, SpreadCurve * [Instruments](../../api/credit/instruments) -- FixedRateBond, CDS, CallableBond, ZeroCouponBond, Bill, AmortizingBond, StepUpBond, PIKBond, CappedFloatRateNote, FloatRateNote, AssetSwap # Spread Analytics (/docs/guides/credit/spread-analytics) Compute interpolated swap spreads (I-spread) and asset swap spreads for bonds against calibrated curves. These measures quantify a bond's yield premium over the reference swap curve. *** Setup [#setup] Create a 5-year fixed-rate bond and calibrate a discount curve for spread analysis. We also build a swap zero-rate curve (LineCurve) for I-spread computation. ```python import datetime import math from vade import ( FixedRateBond, DiscountCurve, LineCurve, Deposit, IRS, Solver, AssetSwap, ) effective = datetime.date(2025, 1, 15) settlement = datetime.date(2025, 6, 16) bond = FixedRateBond( effective=effective, termination="5y", coupon=0.045, frequency="s", settlement_days=1, convention="actactisda", face_value=100.0, ) curve = DiscountCurve( { settlement: 1.0, datetime.date(2025, 9, 16): 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, }, interpolation="log_linear", convention="act360", id="sofr", ) dep = Deposit(effective=settlement, termination="3m", rate=0.0, convention="act360") irs_1y = IRS(effective=settlement, termination="1y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_2y = IRS(effective=settlement, termination="2y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_3y = IRS(effective=settlement, termination="3y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_5y = IRS(effective=settlement, termination="5y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") instruments = [ (dep, 0.0440), (irs_1y, 4.20), (irs_2y, 4.10), (irs_3y, 4.00), (irs_5y, 3.90), ] solver = Solver(curves=[curve], instruments=instruments) result = solver.iterate() assert result.converged calibrated = solver.get_curve(0) # Swap zero-rate curve for I-spread (interpolated swap rates) swap_curve = LineCurve( { settlement: 0.044, datetime.date(2026, 6, 16): 0.042, datetime.date(2027, 6, 16): 0.041, datetime.date(2028, 6, 16): 0.040, datetime.date(2030, 6, 16): 0.039, }, convention="act365f", ) ``` I-Spread [#i-spread] The I-spread (interpolated spread) is the difference between a bond's yield to maturity and the linearly interpolated swap rate at the bond's maturity. Unlike the Z-spread (which shifts the entire discount curve), the I-spread uses a single-point comparison against the swap curve. See [FixedRateBond.i\_spread](../../api/credit/instruments#fixedratebond) API reference. ```python continuation i_sprd = bond.i_spread(settlement, swap_curve) assert isinstance(i_sprd, float) assert -0.05 < i_sprd < 0.05 # reasonable range ``` Asset Swap Spread [#asset-swap-spread] An asset swap packages a bond with an interest rate swap. The asset swap spread is the spread on the floating leg that makes the package NPV-neutral. It measures the bond's credit premium over the swap curve. See [AssetSwap](../../api/credit/instruments#assetswap) API reference. Par Asset Swap [#par-asset-swap] A par asset swap assumes the bond trades at par. The spread compensates for the difference between the bond's fixed coupons and the swap curve. ```python continuation par_asw = AssetSwap(bond=bond, asw_type="par") par_spread = par_asw.rate(curves=calibrated) assert isinstance(par_spread, float) assert math.isfinite(par_spread) ``` Non-Par Asset Swap [#non-par-asset-swap] A non-par (market value) asset swap uses the bond's actual dirty price rather than par. This adjusts the spread for the bond's premium or discount. ```python continuation dirty = float(bond.dirty_price(calibrated, settlement=settlement)) non_par_asw = AssetSwap(bond=bond, asw_type="non_par", dirty_price=dirty) non_par_spread = non_par_asw.rate(curves=calibrated) assert isinstance(non_par_spread, float) assert math.isfinite(non_par_spread) ``` Comparing Spread Measures [#comparing-spread-measures] Different spread measures answer different questions. The Z-spread and I-spread use the bond's yield; asset swap spreads reflect the economics of a hedged position. Here they are side by side for the same bond. ```python continuation clean = float(bond.price(calibrated, settlement=settlement)) z_sprd = float(bond.z_spread(calibrated, price=clean, settlement=settlement)) i_sprd_val = float(bond.i_spread(settlement, swap_curve)) asw_val = float(par_asw.rate(curves=calibrated)) assert isinstance(z_sprd, float) assert isinstance(i_sprd_val, float) assert isinstance(asw_val, float) ``` Basis Swap Spread [#basis-swap-spread] A basis swap exchanges two floating-rate streams with different frequencies (e.g., 3M SOFR vs 6M SOFR). The spread is quoted on one leg to compensate for the tenor basis -- the difference in value between short-tenor and long-tenor floating payments. ```python import datetime import math from vade import ( SingleBasisSwap, SBS, DiscountCurve, Deposit, IRS, Solver, ) settlement = datetime.date(2025, 6, 16) # Construct a 5Y basis swap: quarterly leg1 vs semi-annual leg2 # with a 10bp spread on leg1 sbs = SingleBasisSwap( effective=settlement, termination="5y", frequency="q", leg2_frequency="s", spread=0.001, # 10bp spread on leg1 convention="act360", notional=10_000_000.0, ) # SBS is an alias for SingleBasisSwap assert SBS is SingleBasisSwap # Calibrate a discount curve curve = DiscountCurve( { settlement: 1.0, datetime.date(2025, 9, 16): 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, }, interpolation="log_linear", convention="act360", id="sofr", ) dep = Deposit(effective=settlement, termination="3m", rate=0.0, convention="act360") irs_1y = IRS(effective=settlement, termination="1y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_2y = IRS(effective=settlement, termination="2y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_3y = IRS(effective=settlement, termination="3y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_5y = IRS(effective=settlement, termination="5y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") instruments = [ (dep, 0.0440), (irs_1y, 4.20), (irs_2y, 4.10), (irs_3y, 4.00), (irs_5y, 3.90), ] solver = Solver(curves=[curve], instruments=instruments) result = solver.iterate() assert result.converged calibrated = solver.get_curve(0) # Compute the fair spread and NPV basis_rate = sbs.rate(calibrated) basis_npv = sbs.npv(calibrated) assert isinstance(basis_rate, float) assert math.isfinite(basis_rate) assert isinstance(basis_npv, float) assert math.isfinite(basis_npv) ``` Sign Convention [#sign-convention] Vade and QuantLib return basis swap NPVs with **opposite signs** because they report from different perspectives: | Library | Perspective | Convention | | -------- | ----------------- | -------------------------------- | | **Vade** | Spread-paying leg | Positive NPV = leg1 receives net | | **QL** | Opposite leg | Positive NPV = leg2 receives net | For the same trade, multiplying the Vade NPV by -1 aligns with QuantLib: > **Reference values:** QL NPV = -45,337 vs Vade NPV = +45,209 -- the sign > flip reflects perspective, not a calculation error. The \~0.3% magnitude > difference comes from minor convention differences (day count, business day > adjustment), which are separate from the sign convention. In code, `vade_npv * -1` produces the QuantLib-equivalent value. No code change is needed -- the underlying calculation is correct. The difference is purely which side of the swap the NPV is reported from. Asset Swap Sign Convention [#asset-swap-sign-convention] The asset swap spread sections above show how to compute par and non-par (market-value) spreads. Vade and QuantLib return these spreads with **opposite signs** because they report from different perspectives. | Library | Perspective | Convention | | -------- | ---------------- | ---------------------------------------------- | | **Vade** | Bond-holder | Receives fixed coupons, pays floating + spread | | **QL** | Asset swap buyer | Pays fixed coupons, receives floating + spread | The result is an exact sign flip for both par and non-par variants: > **Reference values:** > > * Par ASW: QL = +5.06 bp, Vade = -5.06 bp > * Market ASW: QL = +3.56 bp, Vade = -3.56 bp Multiplying the Vade spread by -1 aligns with QuantLib. The absolute values match -- the sign difference reflects the bond-holder vs buyer perspective. ```python import datetime import math from vade import ( FixedRateBond, DiscountCurve, Deposit, IRS, Solver, AssetSwap, ) effective = datetime.date(2025, 1, 15) settlement = datetime.date(2025, 6, 16) bond = FixedRateBond( effective=effective, termination="5y", coupon=0.045, frequency="s", settlement_days=1, convention="actactisda", face_value=100.0, ) curve = DiscountCurve( { settlement: 1.0, datetime.date(2025, 9, 16): 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, }, interpolation="log_linear", convention="act360", id="sofr", ) dep = Deposit(effective=settlement, termination="3m", rate=0.0, convention="act360") irs_1y = IRS(effective=settlement, termination="1y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_2y = IRS(effective=settlement, termination="2y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_3y = IRS(effective=settlement, termination="3y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_5y = IRS(effective=settlement, termination="5y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") instruments = [ (dep, 0.0440), (irs_1y, 4.20), (irs_2y, 4.10), (irs_3y, 4.00), (irs_5y, 3.90), ] solver = Solver(curves=[curve], instruments=instruments) result = solver.iterate() assert result.converged calibrated = solver.get_curve(0) # Par asset swap spread par_asw = AssetSwap(bond=bond, asw_type="par") par_spread = par_asw.rate(curves=calibrated) assert isinstance(par_spread, float) assert math.isfinite(par_spread) # Non-par (market value) asset swap spread dirty = float(bond.dirty_price(calibrated, settlement=settlement)) non_par_asw = AssetSwap(bond=bond, asw_type="non_par", dirty_price=dirty) non_par_spread = non_par_asw.rate(curves=calibrated) assert isinstance(non_par_spread, float) assert math.isfinite(non_par_spread) # Both perspectives yield same absolute value -- only sign differs # vade_spread * -1 == ql_spread (for the same trade) ``` Next Steps [#next-steps] * [Callable Bonds](callable-bonds) -- Option-adjusted spread for bonds with embedded options * [Bonds](bonds) -- Core bond analytics and Z-spread # Cross-Currency Swaps (/docs/guides/fx/cross-currency) A [CrossCurrencySwap](../../api/fx/instruments#xcs) (XCS) exchanges floating rate payments in two currencies with notional exchanges at inception and maturity. It uses a 4-curve pricing model -- discount and forecast curves for each leg -- plus spot FX rates for notional conversion. *** Cross-Currency Swap [#cross-currency-swap] A [CrossCurrencySwap](../../api/fx/instruments#xcs) (XCS) exchanges floating rate payments in two currencies with notional exchanges. It uses a 4-curve pricing model: discount and forecast curves for each leg, plus spot FX rates. ```python xcs = XCS( effective=effective, termination="5y", leg1_currency="eur", leg2_currency="usd", leg1_notional=1_000_000.0, leg2_notional=1_080_000.0, initial_fx_rate=1.08, frequency="a", convention="act360", ) float(xcs.npv(eur_curve, eur_curve, usd_curve, usd_curve, fxr)) # 66059.25314189272 float(xcs.rate(eur_curve, eur_curve, usd_curve, usd_curve, fxr)) # -145.8313845574968 ``` Next Steps [#next-steps] * [FX Rates & Forwards](fx-rates-forwards) -- FX rate triangulation and forward construction * [Non-Deliverable Instruments](non-deliverable) -- NDF and non-deliverable swap pricing for restricted currencies # FX Rates & Forwards (/docs/guides/fx/fx-rates-forwards) Set up multi-currency [FXRates](../../api/fx/fx-rates#fxrates) with graph-based triangulation, compute forward exchange rates with [FXForwards](../../api/fx/fx-rates#fxforwards) from interest rate parity, and price tradeable [FXForward](../../api/fx/instruments#fxforward) instruments against discount curves. *** FX Rates and Triangulation [#fx-rates-and-triangulation] [FXRates](../../api/fx/fx-rates#fxrates) discovers all cross rates automatically from a sparse set of quoted pairs. Provide any connected set of currency pairs and query arbitrary crosses -- the triangulation engine finds the shortest path through the currency graph. ```python float(fxr.rate("eurusd")) # 1.08 float(fxr.rate("eurgbp")) # 0.8503937007874016 float(fxr.rate("gbpjpy")) # 189.865 fxr.currencies # ['eur', 'gbp', 'jpy', 'usd'] float(fxr.convert(1_000_000, "eur", "usd")) # 1080000.0 ``` FX Forwards [#fx-forwards] [FXForwards](../../api/fx/fx-rates#fxforwards) computes forward exchange rates from interest rate parity. Given spot rates and discount curves for each currency, the forward rate reflects the interest rate differential over the delivery period. Forward points are the difference between the forward and spot rates. ```python continuation float(fxf.rate("eurusd", datetime.date(2026, 6, 16), curves={"usd": usd_curve, "eur": eur_curve})) # 1.0895470734849848 float(fxf.points("eurusd", datetime.date(2026, 6, 16), curves={"usd": usd_curve, "eur": eur_curve})) # 0.009547073484984736 ``` FX Forward Instrument [#fx-forward-instrument] An [FXForward](../../api/fx/instruments#fxforward) is a tradeable instrument that locks in an exchange rate for a future delivery date. It prices against two discount curves (one per currency) and the spot FX rates. ```python continuation fwd = FXForward(pair="eurusd", notional=1_000_000.0, delivery=datetime.date(2026, 6, 16)) float(fwd.npv(usd_curve, eur_curve, fxr)) # 8487.043853479321 float(fwd.rate(usd_curve, eur_curve, fxr)) # 1.0705365820213681 ``` Next Steps [#next-steps] * [Cross-Currency Swaps](cross-currency) -- Cross-currency swap pricing and risk * [Non-Deliverable Instruments](non-deliverable) -- NDF and non-deliverable swap pricing for restricted currencies # FX (/docs/guides/fx) Vade's FX analytics cover spot rate triangulation, forward rate construction, cross-currency swaps, and non-deliverable instruments. Build consistent FX rate systems from a sparse set of market quotes, derive implied curves from FX forwards, and price instruments across currency pairs including restricted emerging market currencies. *** What's Covered [#whats-covered] * **Spot rates** -- FXRates with automatic triangulation across any currency pair from a base set of quotes * **Forwards** -- FXForwards and FXForward for forward rate construction from interest rate differentials * **Cross-currency swaps** -- XCS pricing with dual-curve discounting and FX basis * **Non-deliverable instruments** -- NDF (non-deliverable forward), NDIRS (non-deliverable interest rate swap), NDXCS (non-deliverable cross-currency swap) * **Implied curves** -- FXImpliedCurve deriving discount factors from FX forwards and a reference curve Key Concepts [#key-concepts] * **Spot rate** -- the exchange rate for immediate (T+2) settlement between two currencies * **Forward points** -- the difference between the FX forward rate and the spot rate, driven by interest rate differentials * **Cross rate** -- an exchange rate derived from two other rates via a common currency (e.g., EUR/JPY from EUR/USD and USD/JPY) * **Triangulation** -- the process of computing cross rates through an intermediate currency to maintain no-arbitrage consistency * **Covered interest parity** -- the relationship linking spot rates, forward rates, and interest rate differentials across currencies * **Non-deliverable forward** -- an FX forward settled in a convertible currency rather than the restricted currency, common for emerging markets (BRL, INR, KRW) * **Fixing** -- the official exchange rate published by a central bank or industry body, used for NDF settlement Quick Taste [#quick-taste] ```python fxr = FXRates(fx_rates={"eurusd": 1.08, "usdjpy": 149.50}, settlement=effective) cross = float(fxr.rate("eurjpy")) # Triangulated cross rate: EUR/JPY = EUR/USD * USD/JPY assert abs(cross - 1.08 * 149.50) < 0.01 ``` Guides [#guides] * [FX Rates & Forwards](./fx-rates-forwards) -- FX rate triangulation and forward construction * [Cross-Currency Swaps](./cross-currency) -- Cross-currency swap pricing and risk * [Non-Deliverable Instruments](./non-deliverable) -- NDF and non-deliverable swap pricing for restricted currencies API Reference [#api-reference] * [FX](../../api/fx/fx-rates) -- FXRates, FXForwards, FXPair * [Instruments](../../api/fx/instruments) -- XCS, FXForward, NDF, NDIRS, NDXCS * [Curves](../../api/rates/curves) -- FXImpliedCurve # Non-Deliverable Instruments (/docs/guides/fx/non-deliverable) Non-deliverable (ND) instruments settle in a convertible currency rather than the restricted currency where cashflows accrue. This is common for emerging market currencies (BRL, INR, KRW, CNY, TWD) where currency controls limit deliverability. Vade provides three ND instrument types: * **[NDF](../../api/fx/instruments#ndf)** -- Non-Deliverable Forward (single FX net settlement) * **[NDIRS](../../api/fx/instruments#ndirs)** -- Non-Deliverable Interest Rate Swap (per-period FX conversion) * **[NDXCS](../../api/fx/instruments#ndxcs)** -- Non-Deliverable Cross-Currency Swap (ND leg with FX conversion) All three share a unified FX fixing mechanism: a `fx_fixings` dict mapping dates to known [FXRates](../../api/fx/fx-rates#fxrates), with IRP-implied forward fallback for missing dates. Setup [#setup] Build USD and BRL curves for the examples: ```python usd_curve = DiscountCurve( { datetime.date(2025, 6, 16): 1.0, datetime.date(2026, 6, 16): 0.9615, datetime.date(2027, 6, 16): 0.9246, datetime.date(2028, 6, 16): 0.8890, }, interpolation="log_linear", convention="act360", id="usd", ) brl_curve = DiscountCurve( { datetime.date(2025, 6, 16): 1.0, datetime.date(2026, 6, 16): 0.8929, datetime.date(2027, 6, 16): 0.7972, datetime.date(2028, 6, 16): 0.7118, }, interpolation="log_linear", convention="act360", id="brl", ) fxr = FXRates({"usdbrl": 5.20}, settlement=datetime.date(2025, 6, 16)) ``` NDF -- Non-Deliverable Forward [#ndf----non-deliverable-forward] An NDF is the simplest ND instrument: a single forward FX exchange that settles as a net cash payment in the settlement currency. Construction [#construction] ```python continuation ndf = NDF( spec="usdbrl_ndf", effective=datetime.date(2025, 6, 16), delivery=datetime.date(2025, 12, 16), notional=1_000_000.0, contracted_rate=5.25, ) ``` Pricing [#pricing] The fair rate is the IRP-implied forward. NPV is the discounted net settlement: ```python continuation fair_rate = ndf.rate(usd_curve, brl_curve, fxr) npv = ndf.npv(usd_curve, brl_curve, fxr) assert abs(float(fair_rate)) < 10 assert abs(float(npv)) < 1e8 ``` Cashflows [#cashflows] ```python continuation cf = ndf.cashflows(usd_curve, brl_curve, fxr) assert len(cf) >= 1 ``` Spec-Based Construction [#spec-based-construction] Five market convention specs are available: ```python continuation for spec in ["usdbrl_ndf", "usdcny_ndf", "usdinr_ndf", "usdkrw_ndf", "usdtwd_ndf"]: ndf_i = NDF(spec=spec, effective=datetime.date(2025, 6, 16), delivery=datetime.date(2025, 12, 16), notional=1e6, contracted_rate=5.25) assert ndf_i is not None ``` NDIRS -- Non-Deliverable Interest Rate Swap [#ndirs----non-deliverable-interest-rate-swap] An NDIRS is an IRS where both legs accrue in a restricted currency but settle in a convertible currency. Each period's cashflow is converted via an FX rate (from fixings or IRP-implied forward). Construction [#construction-1] ```python continuation ndirs = NDIRS( spec="usdbrl_ndirs", effective=datetime.date(2025, 6, 16), termination="2Y", fixed_rate=10.0, notional=1_000_000.0, ) ``` 3-Curve Pricing [#3-curve-pricing] NDIRS uses three curves: * Settlement discount (USD) -- for NPV discounting * Restricted forecast (BRL) -- for floating rate projection * Restricted discount (BRL) -- for IRP forward derivation ```python continuation par_rate = ndirs.rate(curves=[usd_curve, brl_curve, brl_curve], fx_rates=fxr) npv = ndirs.npv(curves=[usd_curve, brl_curve, brl_curve], fx_rates=fxr) assert 0 < float(par_rate) < 30 assert abs(float(npv)) < 1e8 ``` Partial FX Fixings [#partial-fx-fixings] For past periods with known FX fixings, pass a date-keyed dict: ```python continuation ndirs_fix = NDIRS( spec="usdbrl_ndirs", effective=datetime.date(2025, 6, 16), termination="2Y", fixed_rate=10.0, fx_fixings={datetime.date(2025, 12, 16): 5.35}, ) npv_fix = ndirs_fix.npv(curves=[usd_curve, brl_curve, brl_curve], fx_rates=fxr) assert abs(float(npv_fix)) < 1e8 ``` Cashflows with Settlement Columns [#cashflows-with-settlement-columns] NDIRS cashflows include FX rate and settlement amount columns: ```python continuation cf = ndirs.cashflows(curves=[usd_curve, brl_curve, brl_curve], fx_rates=fxr) assert len(cf) > 0 ``` NDXCS -- Non-Deliverable Cross-Currency Swap [#ndxcs----non-deliverable-cross-currency-swap] An NDXCS is a cross-currency swap where one leg is non-deliverable. Cashflows on the ND leg are converted via per-period FX fixings and settled in the convertible currency. Supports notional exchanges and MTM resets. Construction [#construction-2] ```bash ndxcs = NDXCS( effective=datetime.date(2025, 6, 16), termination="2Y", leg1_currency="USD", leg2_currency="BRL", settlement_currency="usd", nd_leg=2, pair="usdbrl", leg1_notional=1_000_000.0, leg2_notional=5_200_000.0, initial_fx_rate=5.2, frequency="q", convention="act360", notional_exchange=True, ) ``` Pricing [#pricing-1] NDXCS uses 4 curves (leg1 disc/forecast, leg2 disc/forecast) plus FXRates: ```bash rate = ndxcs.rate(usd_curve, usd_curve, brl_curve, brl_curve, fxr) npv = ndxcs.npv(usd_curve, usd_curve, brl_curve, brl_curve, fxr) assert abs(float(rate)) < 500 assert abs(float(npv)) < 1e8 ``` Spec-Based Construction [#spec-based-construction-1] Five market convention specs are available: ```bash ndxcs_spec = NDXCS( spec="usdbrl_ndxcs", effective=datetime.date(2025, 6, 16), termination="1Y", leg1_notional=1_000_000.0, leg2_notional=5_200_000.0, initial_fx_rate=5.2, ) assert ndxcs_spec is not None ``` FX Fixings for Notional Exchanges [#fx-fixings-for-notional-exchanges] The same `fx_fixings` dict covers period cashflows, notional exchanges, and MTM resets. Pass a known fixing for the exchange date: ```bash ndxcs_fix = NDXCS( effective=datetime.date(2025, 6, 16), termination="1Y", leg1_currency="USD", leg2_currency="BRL", settlement_currency="usd", nd_leg=2, pair="usdbrl", leg1_notional=1_000_000.0, leg2_notional=5_200_000.0, initial_fx_rate=5.2, fx_fixings={datetime.date(2025, 6, 16): 5.20}, ) npv_fix = ndxcs_fix.npv(usd_curve, usd_curve, brl_curve, brl_curve, fxr) assert abs(float(npv_fix)) < 1e8 ``` Cashflows [#cashflows-1] ```bash cf = ndxcs.cashflows(usd_curve, usd_curve, brl_curve, brl_curve, fxr) assert len(cf) > 0 ``` JSON Serialization [#json-serialization] All ND instruments support JSON serialization: ```python continuation json_str = ndf.to_json() assert isinstance(json_str, str) and len(json_str) > 10 ndf_rt = NDF.from_json(json_str) npv_rt = ndf_rt.npv(usd_curve, brl_curve, fxr) assert abs(float(ndf.npv(usd_curve, brl_curve, fxr)) - float(npv_rt)) < 1e-6 ``` NDIRS also supports round-trip serialization: ```python continuation ndirs_json = ndirs.to_json() ndirs_rt = NDIRS.from_json(ndirs_json) npv_orig = ndirs.npv(curves=[usd_curve, brl_curve, brl_curve], fx_rates=fxr) npv_rt = ndirs_rt.npv(curves=[usd_curve, brl_curve, brl_curve], fx_rates=fxr) assert abs(float(npv_orig) - float(npv_rt)) < 1e-6 ``` AD Support and Risk [#ad-support-and-risk] All ND instruments return AD-enabled types for automatic differentiation. FXRates creates Dual variables, enabling delta and gamma computation: ```python continuation rate_val = ndf.rate(usd_curve, brl_curve, fxr) # rate_val is Dual when FXRates uses AD -- float(rate_val) extracts the real part assert math.isfinite(float(rate_val)) ``` # Bootstrap & Parametric Curves (/docs/guides/rates/bootstrap-parametric) Use [bootstrap](../../api/rates/solver#bootstrap) for node-by-node curve stripping via Brent root-finding, and the [NelsonSiegel](../../api/rates/curves#nelsonsiegel) family for analytical curve models with closed-form zero rate formulas. *** Iterative Bootstrap [#iterative-bootstrap] The `bootstrap()` function calibrates a curve one node at a time using Brent's method, compared to the Solver which optimizes all nodes simultaneously via matrix-based methods. Bootstrap is conceptually simpler and well-suited for single-curve scenarios where each instrument pins exactly one node. Setup [#setup] ```python import datetime from vade import bootstrap, DiscountCurve, Deposit, FRA, IRS effective = datetime.date(2025, 6, 16) curve = DiscountCurve( { effective: 1.0, datetime.date(2025, 12, 16): 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, }, interpolation="log_linear", convention="act360", ) dep_6m = Deposit(effective=effective, termination="6m", rate=0.0, convention="act360") irs_1y = IRS(effective=effective, termination="1y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_2y = IRS(effective=effective, termination="2y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_3y = IRS(effective=effective, termination="3y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_5y = IRS(effective=effective, termination="5y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") ``` Calibration [#calibration] Unlike the Solver which takes `(instrument, target)` tuples, `bootstrap()` takes a list of dicts with `"instrument"`, `"target"`, and `"disc_curve_idx"` keys. The instrument value must be the internal Rust binding accessed via `._inner`. ```python continuation calibrated = bootstrap( curves=[curve], instruments=[ {"instrument": dep_6m._inner, "target": 0.0425, "disc_curve_idx": 0}, {"instrument": irs_1y._inner, "target": 4.00, "disc_curve_idx": 0}, {"instrument": irs_2y._inner, "target": 3.85, "disc_curve_idx": 0}, {"instrument": irs_3y._inner, "target": 3.75, "disc_curve_idx": 0}, {"instrument": irs_5y._inner, "target": 3.70, "disc_curve_idx": 0}, ], target_curve_idx=0, bracket_low=0.5, bracket_high=1.5, ) float(calibrated.discount_factor(datetime.date(2026, 6, 16))) # 0.9610212146820518 float(calibrated.discount_factor(datetime.date(2030, 6, 16))) # 0.8319003344593215 # Verify repricing: instrument rates match the original targets abs(float(dep_6m.rate(calibrated)) - 0.0425) < 1e-6 # True abs(float(irs_1y.rate(calibrated)) - 4.00) < 1e-4 # True abs(float(irs_5y.rate(calibrated)) - 3.70) < 1e-4 # True ``` *** Parametric Curves [#parametric-curves] Parametric curves use analytical formulas with a small number of parameters instead of interpolating between node-based discount factors. They produce smooth yield curves and are commonly used for scenario analysis, regulatory reporting, and fitting observed market yields. Nelson-Siegel [#nelson-siegel] The Nelson-Siegel model has four parameters: `beta0` (long-term rate), `beta1` (short-term spread), `beta2` (curvature hump), and `tau` (decay factor controlling hump location). ```python continuation from vade import NelsonSiegel, NelsonSiegelSvensson, SmithWilson ns = NelsonSiegel( beta0=0.04, beta1=-0.02, beta2=0.03, tau=1.5, base_date=effective, convention="act365f", ) float(ns.zero_rate(effective, datetime.date(2026, 6, 16))) # 0.03189622964353336 float(ns.zero_rate(effective, datetime.date(2030, 6, 16))) # 0.041823322038103426 float(ns.zero_rate(effective, datetime.date(2045, 6, 16))) # 0.0407494373039477 float(ns.discount_factor(datetime.date(2030, 6, 16))) # 0.8112076671017623 ``` Nelson-Siegel-Svensson [#nelson-siegel-svensson] The Svensson extension adds `beta3` and `tau2` for a second curvature hump, providing more flexibility at intermediate tenors. ```python continuation nss = NelsonSiegelSvensson( beta0=0.04, beta1=-0.02, beta2=0.03, beta3=0.01, tau1=1.5, tau2=5.0, base_date=effective, convention="act365f", ) float(nss.zero_rate(effective, datetime.date(2026, 6, 16))) # 0.032772384458854435 float(nss.zero_rate(effective, datetime.date(2030, 6, 16))) # 0.04446630078480113 float(nss.zero_rate(effective, datetime.date(2045, 6, 16))) # 0.04301943830572509 ``` Smith-Wilson [#smith-wilson] The [SmithWilson](../../api/rates/curves#smithwilson) model is used in Solvency II regulation. It takes market data points (dates and rates) and extrapolates toward an ultimate forward rate (UFR). The `alpha` parameter controls how quickly the curve converges to the UFR beyond the last liquid point. ```python continuation dates = [ datetime.date(2026, 6, 16), datetime.date(2028, 6, 16), datetime.date(2030, 6, 16), datetime.date(2035, 6, 16), ] rates = [0.038, 0.039, 0.037, 0.036] sw = SmithWilson( dates=dates, rates=rates, ufr=0.042, alpha=0.1, base_date=effective, convention="act365f", ) float(sw.discount_factor(datetime.date(2030, 6, 16))) # 0.8310200391947283 float(sw.discount_factor(datetime.date(2050, 6, 16))) # 0.3866408502017419 float(sw.discount_factor(datetime.date(2075, 6, 16))) # 0.1366164115809102 ``` Next Steps [#next-steps] * [Curve Building](curve-building) -- Interpolation methods and forward rate analysis * [Serialization](../serialization) -- Save and restore parametric curves via JSON # Caps & Floors (/docs/guides/rates/capfloor) Price interest rate caps and floors using [CapFloor](../../api/rates/instruments#capfloor) with Black-76 (lognormal) and Bachelier (normal) volatility models. Caps pay when the floating rate exceeds the strike; floors pay when it falls below. *** Setup [#setup] Cap and floor pricing requires a discount curve for present-valuing cashflows and a forecast curve for projecting forward rates. Here we calibrate a single curve and use it for both roles. ```python import datetime from vade import CapFloor, DiscountCurve, Deposit, IRS, Solver effective = datetime.date(2025, 6, 16) curve = DiscountCurve( { effective: 1.0, datetime.date(2025, 9, 16): 1.0, datetime.date(2025, 12, 16): 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, }, interpolation="log_linear", convention="act360", id="sofr", ) dep_3m = Deposit(effective=effective, termination="3m", rate=0.0, convention="act360") dep_6m = Deposit(effective=effective, termination="6m", rate=0.0, convention="act360") irs_1y = IRS(effective=effective, termination="1y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_2y = IRS(effective=effective, termination="2y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_3y = IRS(effective=effective, termination="3y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_5y = IRS(effective=effective, termination="5y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") solver = Solver( curves=[curve], instruments=[ (dep_3m, 0.0430), (dep_6m, 0.0425), (irs_1y, 4.10), (irs_2y, 3.95), (irs_3y, 3.85), (irs_5y, 3.80), ], ) result = solver.iterate() assert result.converged disc_curve = solver.get_curve(0) forecast_curve = disc_curve ``` Cap Pricing with Black-76 [#cap-pricing-with-black-76] The Black-76 model prices each caplet using a lognormal volatility assumption. Set `model="black76"` and provide an implied volatility in lognormal terms (e.g., 0.20 for 20%). ```python continuation cap = CapFloor( effective=effective, termination="5y", strike=0.04, vol=0.20, notional=1_000_000.0, frequency="q", cap_floor_type="cap", model="black76", convention="act360", ) float(cap.npv(disc_curve, forecast_curve)) # 169497.62285527054 ``` Floor Pricing [#floor-pricing] A floor is the mirror of a cap -- it pays when the floating rate falls below the strike. With forward rates near 4% and a strike of 4%, the floor is deeply out of the money and has negligible value. ```python continuation floor = CapFloor( effective=effective, termination="5y", strike=0.04, vol=0.20, notional=1_000_000.0, frequency="q", cap_floor_type="floor", model="black76", convention="act360", ) floor_npv = float(floor.npv(disc_curve, forecast_curve)) floor_npv < 1.0 # True -- floor is deep OTM cap_npv = float(cap.npv(disc_curve, forecast_curve)) cap_minus_floor = cap_npv - floor_npv cap_minus_floor > 0 # True -- put-call parity: cap - floor approximates swap value ``` Bachelier Model [#bachelier-model] The Bachelier (normal) model uses an absolute volatility in rate terms rather than a lognormal percentage. A typical normal vol is around 50 basis points (0.005), compared to 20% (0.20) for Black-76. Set `model="bachelier"`. ```python continuation cap_bach = CapFloor( effective=effective, termination="5y", strike=0.04, vol=0.005, notional=1_000_000.0, frequency="q", cap_floor_type="cap", model="bachelier", convention="act360", ) float(cap_bach.npv(disc_curve, forecast_curve)) # 169498.43835327454 ``` Strike Sensitivity [#strike-sensitivity] Cap value decreases as the strike increases -- a higher strike means fewer caplets finish in the money. This relationship is visible across a range of strikes. ```python continuation for strike in [0.03, 0.035, 0.04, 0.045, 0.05]: c = CapFloor( effective=effective, termination="5y", strike=strike, vol=0.20, notional=1_000_000.0, frequency="q", cap_floor_type="cap", model="black76", convention="act360", ) npv = float(c.npv(disc_curve, forecast_curve)) # Strike 3.0%: higher cap value # Strike 5.0%: lower cap value assert float( CapFloor(effective=effective, termination="5y", strike=0.03, vol=0.20, notional=1_000_000.0, frequency="q", cap_floor_type="cap", model="black76", convention="act360").npv(disc_curve, forecast_curve) ) > float( CapFloor(effective=effective, termination="5y", strike=0.05, vol=0.20, notional=1_000_000.0, frequency="q", cap_floor_type="cap", model="black76", convention="act360").npv(disc_curve, forecast_curve) ) ``` Next Steps [#next-steps] * [FX Rates & Forwards](../fx/fx-rates-forwards) -- FX rate triangulation and forward construction * [Calibration](../calibration) -- Multi-curve calibration with Solver # Curve Building (/docs/guides/rates/curve-building) Explore how interpolation method choice affects forward rates, and learn to work with different curve types. This guide builds a calibrated [DiscountCurve](../../api/rates/curves#discountcurve) and then reconstructs it with three interpolation methods to compare their forward rate profiles. *** Market Data and Calibration [#market-data-and-calibration] Set up a USD SOFR market dataset and calibrate a discount curve. The dataset covers tenors from 1-month deposits through 10-year swaps. ```python import datetime from vade import DiscountCurve, IRS, FRA, Deposit, Solver effective = datetime.date(2025, 6, 16) nodes = { effective: 1.0, datetime.date(2025, 7, 16): 1.0, datetime.date(2025, 9, 16): 1.0, datetime.date(2025, 12, 16): 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, datetime.date(2032, 6, 16): 1.0, datetime.date(2035, 6, 16): 1.0, } curve = DiscountCurve( nodes, interpolation="log_linear", convention="act360", id="sofr" ) dep_1m = Deposit(effective=effective, termination="1m", rate=0.0, convention="act360") fra_3m = FRA(effective=effective, termination="3m", fixed_rate=0.0, convention="act360") fra_6m = FRA(effective=effective, termination="6m", fixed_rate=0.0, convention="act360") irs_1y = IRS(effective=effective, termination="1y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_2y = IRS(effective=effective, termination="2y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_3y = IRS(effective=effective, termination="3y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_5y = IRS(effective=effective, termination="5y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_7y = IRS(effective=effective, termination="7y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_10y = IRS(effective=effective, termination="10y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") instruments = [ (dep_1m, 0.0430), (fra_3m, 0.0425), (fra_6m, 0.0415), (irs_1y, 4.00), (irs_2y, 3.85), (irs_3y, 3.75), (irs_5y, 3.70), (irs_7y, 3.75), (irs_10y, 3.85), ] solver = Solver(curves=[curve], instruments=instruments) result = solver.iterate() assert result.converged calibrated = solver.get_curve(0) float(calibrated.discount_factor(datetime.date(2026, 6, 16))) # 0.9610212208394634 float(calibrated.discount_factor(datetime.date(2030, 6, 16))) # 0.8319869245862279 ``` Querying the Curve [#querying-the-curve] A calibrated curve supports three core queries: discount factors, zero rates, and forward rates. All rates use the [act360](../../conventions#day-count-conventions) day count convention specified at construction. ```python import datetime from vade import DiscountCurve, IRS, FRA, Deposit, Solver effective = datetime.date(2025, 6, 16) nodes = { effective: 1.0, datetime.date(2025, 7, 16): 1.0, datetime.date(2025, 9, 16): 1.0, datetime.date(2025, 12, 16): 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, datetime.date(2032, 6, 16): 1.0, datetime.date(2035, 6, 16): 1.0, } curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360", id="sofr") dep_1m = Deposit(effective=effective, termination="1m", rate=0.0, convention="act360") fra_3m = FRA(effective=effective, termination="3m", fixed_rate=0.0, convention="act360") fra_6m = FRA(effective=effective, termination="6m", fixed_rate=0.0, convention="act360") irs_1y = IRS(effective=effective, termination="1y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_2y = IRS(effective=effective, termination="2y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_3y = IRS(effective=effective, termination="3y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_5y = IRS(effective=effective, termination="5y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_7y = IRS(effective=effective, termination="7y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_10y = IRS(effective=effective, termination="10y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") instruments = [ (dep_1m, 0.0430), (fra_3m, 0.0425), (fra_6m, 0.0415), (irs_1y, 4.00), (irs_2y, 3.85), (irs_3y, 3.75), (irs_5y, 3.70), (irs_7y, 3.75), (irs_10y, 3.85), ] solver = Solver(curves=[curve], instruments=instruments) result = solver.iterate() calibrated = solver.get_curve(0) # Discount factors float(calibrated.discount_factor(datetime.date(2026, 6, 16))) # 0.9610212208394634 float(calibrated.discount_factor(datetime.date(2030, 6, 16))) # 0.8319869245862279 float(calibrated.discount_factor(datetime.date(2035, 6, 16))) # 0.680542109248269 # Zero rates (continuously compounded, annualized) float(calibrated.zero_rate(effective, datetime.date(2026, 6, 16))) # 0.039214150340492254 float(calibrated.zero_rate(effective, datetime.date(2030, 6, 16))) # 0.03626390009498523 # Forward rates between future dates float(calibrated.forward_rate(datetime.date(2026, 6, 16), datetime.date(2027, 6, 16))) # 0.03627210043804068 float(calibrated.forward_rate(datetime.date(2027, 6, 16), datetime.date(2028, 6, 16))) # 0.03477612714625993 float(calibrated.forward_rate(datetime.date(2030, 6, 16), datetime.date(2032, 6, 16))) # 0.038181993768805886 ``` Interpolation Comparison [#interpolation-comparison] Rebuild the curve from calibrated nodes using three different interpolation methods to see how they affect forward rates between nodes. The calibrated discount factors at node dates are the same regardless of interpolation -- differences appear only between nodes. [Log-linear](../../conventions#interpolation-methods) interpolation produces piecewise constant forward rates between nodes. [Linear-zero-rate](../../conventions#interpolation-methods) interpolation linearly interpolates in zero-rate space, producing smoother forward rates. [Flat-forward](../../conventions#interpolation-methods) is a step function that holds the forward rate constant within each node interval. ```python import datetime from vade import DiscountCurve cal_nodes = { datetime.date(2025, 6, 16): 1.0, datetime.date(2025, 7, 16): 0.9964294610977332, datetime.date(2025, 9, 16): 0.9891976578024332, datetime.date(2025, 12, 16): 0.9791251319654892, datetime.date(2026, 6, 16): 0.9610212208394634, datetime.date(2027, 6, 16): 0.9263207970596119, datetime.date(2028, 6, 16): 0.89414224902702, datetime.date(2030, 6, 16): 0.8319869245862279, datetime.date(2032, 6, 16): 0.7699195851637843, datetime.date(2035, 6, 16): 0.680542109248269, } curve_ll = DiscountCurve(cal_nodes, interpolation="log_linear", convention="act360") curve_lz = DiscountCurve(cal_nodes, interpolation="linear_zero_rate", convention="act360") curve_ff = DiscountCurve(cal_nodes, interpolation="flat_forward", convention="act360") # Forward rates between 18M and 2Y (between the 1Y and 2Y nodes) start_1 = datetime.date(2026, 12, 16) end_1 = datetime.date(2027, 6, 16) float(curve_ll.forward_rate(start_1, end_1)) # 0.03627210043804068 float(curve_lz.forward_rate(start_1, end_1)) # 0.035534583782283175 float(curve_ff.forward_rate(start_1, end_1)) # 0.07274350093003831 # Forward rates between 4Y and 5Y (between the 3Y and 5Y nodes) start_2 = datetime.date(2029, 6, 16) end_2 = datetime.date(2030, 6, 16) float(curve_ll.forward_rate(start_2, end_2)) # 0.035530567199720826 float(curve_lz.forward_rate(start_2, end_2)) # 0.035285822519282804 float(curve_ff.forward_rate(start_2, end_2)) # 0.07106113439944166 ``` At node dates, all three methods agree because the underlying discount factors are identical. Between nodes, the methods diverge significantly -- log-linear produces smooth constant forward rates within each interval, linear-zero-rate varies gradually, and flat-forward creates large jumps at node boundaries. LineCurve [#linecurve] [LineCurve](../../api/rates/curves#linecurve) stores zero rates directly instead of discount factors. This is useful for representing rate curves, spreads, or any quantity that should be interpolated linearly in its native space. ```python import datetime from vade import LineCurve effective = datetime.date(2025, 6, 16) line_curve = LineCurve( { effective: 0.040, datetime.date(2026, 6, 16): 0.039, datetime.date(2027, 6, 16): 0.038, datetime.date(2030, 6, 16): 0.037, }, interpolation="linear", convention="act360", ) float(line_curve.discount_factor(datetime.date(2026, 6, 16))) # 0.9612299019061498 float(line_curve.zero_rate(effective, datetime.date(2026, 6, 16))) # 0.039 float(line_curve.zero_rate(effective, datetime.date(2027, 6, 16))) # 0.038 ``` ForwardCurve [#forwardcurve] [ForwardCurve](../../api/rates/curves#forwardcurve) stores instantaneous forward rates. Nodes represent continuously compounded rates on a per-day basis -- to express an annualized 4% rate, divide by 365. Discount factors are computed by integrating the forward rate function. ```python import datetime from vade import ForwardCurve effective = datetime.date(2025, 6, 16) fwd_curve = ForwardCurve( { effective: 0.04 / 365, datetime.date(2026, 6, 16): 0.038 / 365, datetime.date(2027, 6, 16): 0.037 / 365, datetime.date(2030, 6, 16): 0.036 / 365, }, interpolation="flat_forward", convention="act365f", ) float(fwd_curve.discount_factor(datetime.date(2026, 6, 16))) # 0.9607894391523232 float(fwd_curve.forward_rate(effective, datetime.date(2026, 6, 16))) # 0.04 float(fwd_curve.forward_rate(datetime.date(2026, 6, 16), datetime.date(2027, 6, 16))) # 0.038 ``` Next Steps [#next-steps] * [Pricing](pricing) -- price instruments against calibrated curves * [Calibration](../calibration) -- multi-curve frameworks and solver tuning # Rates (/docs/guides/rates) Vade's rates analytics cover interest rate curve construction, instrument pricing, risk sensitivities, and volatility products. Build discount and forward curves from market data, price linear and non-linear derivatives, and compute AD-powered greeks -- all from a single calibrated curve object. *** What's Covered [#whats-covered] * **Curves** -- DiscountCurve, ForwardCurve, LineCurve with configurable interpolation (log-linear, log-cubic, linear-zero) * **Parametric curves** -- NelsonSiegel, NelsonSiegelSvensson, SmithWilson analytical models * **Linear instruments** -- IRS, FRA, ZCS, OIS, Deposit, IRFuture * **Non-linear instruments** -- CapFloor (caps and floors with Black-76 and Bachelier models) * **Calibration** -- Multi-instrument Solver (Levenberg-Marquardt, Gauss-Newton, gradient descent) and single-curve bootstrap * **Risk** -- AD-based delta, gamma, and tenor-bucketed sensitivities via IRImpliedCurve Key Concepts [#key-concepts] * **Discount factor** -- the present value of one unit of currency received at a future date, derived from the calibrated curve * **Forward rate** -- the implied interest rate between two future dates, extracted from the discount curve * **Swap rate** -- the fixed rate that makes an interest rate swap's NPV zero at inception * **Day count convention** -- the rule for counting accrued days between dates (act360, act365f, 30360) * **Interpolation** -- the method used to estimate discount factors between known curve nodes * **Bootstrap** -- iterative node-by-node curve stripping using root-finding on each instrument in sequence * **Par rate** -- the coupon rate at which an instrument prices to par (NPV = 0) * **Zero rate** -- the continuously compounded rate implied by a single discount factor over a given tenor Quick Taste [#quick-taste] ```python spot = float(calibrated.discount_factor(effective)) fwd_1y = float(calibrated.discount_factor(datetime.date(2026, 6, 16))) # Discount factor decays from 1.0 as tenor increases assert spot > fwd_1y ``` Guides [#guides] * [Curve Building](./curve-building) -- Interpolation method comparison and forward rate profiles from a calibrated DiscountCurve * [Bootstrap & Parametric Curves](./bootstrap-parametric) -- Node-by-node bootstrap via Brent root-finding and NelsonSiegel analytical models * [Pricing](./pricing) -- Price IRS, FRA, ZCS, OIS, and Deposit against calibrated curves * [Risk](./risk) -- AD-based delta, gamma, and tenor-bucketed risk sensitivities * [Caps & Floors](./capfloor) -- Cap and floor pricing with Black-76 and Bachelier volatility models API Reference [#api-reference] * [Curves](../../api/rates/curves) -- DiscountCurve, LineCurve, ForwardCurve, parametric models (NelsonSiegel, SmithWilson) * [Instruments](../../api/rates/instruments) -- IRS, FRA, ZCS, OIS, Deposit, IRFuture, CapFloor * [Solver](../../api/rates/solver) -- Multi-curve calibration, bootstrap, and Levenberg-Marquardt solver * [Cashflows](../../api/cashflows) -- Leg construction, period types, and accrual conventions # Pricing (/docs/guides/rates/pricing) Price interest rate instruments against calibrated curves using `.rate()`, `.npv()`, and `.cashflows()`. This guide covers [IRS](../../api/rates/instruments#irs), FRA, ZCS, OIS, and Deposit pricing against a [DiscountCurve](../../api/rates/curves#discountcurve) calibrated from USD SOFR market data. *** Setup -- Calibrate a Curve [#setup----calibrate-a-curve] Calibrate the shared USD SOFR dataset to produce a discount curve for pricing. ```python import datetime from vade import DiscountCurve, IRS, FRA, ZCS, OIS, Deposit, Solver effective = datetime.date(2025, 6, 16) nodes = { effective: 1.0, datetime.date(2025, 7, 16): 1.0, datetime.date(2025, 9, 16): 1.0, datetime.date(2025, 12, 16): 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, datetime.date(2032, 6, 16): 1.0, datetime.date(2035, 6, 16): 1.0, } curve = DiscountCurve( nodes, interpolation="log_linear", convention="act360", id="sofr" ) dep_1m = Deposit(effective=effective, termination="1m", rate=0.0, convention="act360") fra_3m = FRA(effective=effective, termination="3m", fixed_rate=0.0, convention="act360") fra_6m = FRA(effective=effective, termination="6m", fixed_rate=0.0, convention="act360") irs_1y = IRS(effective=effective, termination="1y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_2y = IRS(effective=effective, termination="2y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_3y = IRS(effective=effective, termination="3y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_5y = IRS(effective=effective, termination="5y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_7y = IRS(effective=effective, termination="7y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") irs_10y = IRS(effective=effective, termination="10y", frequency="a", fixed_rate=0.0, convention="act360", float_convention="act360") instruments = [ (dep_1m, 0.0430), (fra_3m, 0.0425), (fra_6m, 0.0415), (irs_1y, 4.00), (irs_2y, 3.85), (irs_3y, 3.75), (irs_5y, 3.70), (irs_7y, 3.75), (irs_10y, 3.85), ] solver = Solver(curves=[curve], instruments=instruments) result = solver.iterate() assert result.converged calibrated = solver.get_curve(0) ``` IRS Pricing [#irs-pricing] Price a 5-year interest rate swap with a slightly off-market fixed rate. The `.rate()` method returns the par rate implied by the curve (in percentage form), and `.npv()` shows the mark-to-market value. ```python irs = IRS( effective=effective, termination="5y", frequency="a", fixed_rate=3.80, convention="act360", float_convention="act360", ) float(irs.rate(calibrated)) # 3.7000000000002484 float(irs.npv(calibrated)) # -4542.843986271881 ``` The par rate is 3.70% -- the curve-implied fair fixed rate. Because our swap pays 3.80% fixed (above par), its NPV is negative from the fixed-rate payer's perspective. FRA Pricing [#fra-pricing] Price a forward rate agreement covering the 3-month to 6-month period. [FRA](../../api/rates/instruments#fra) `.rate()` returns the implied forward rate in decimal form. ```python fra = FRA( effective=datetime.date(2025, 9, 16), termination=datetime.date(2025, 12, 16), fixed_rate=4.20, convention="act360", ) float(fra.rate(calibrated)) # 0.040488991946056214 float(fra.npv(calibrated)) # -323.57645004154983 ``` ZCS Pricing [#zcs-pricing] Price a 3-year zero coupon swap. [ZCS](../../api/rates/instruments#zcs) `.rate()` returns the par zero-coupon rate in percentage form. ```python zcs = ZCS( effective=effective, termination="3y", fixed_rate=3.80, convention="act360", float_convention="act360", ) float(zcs.rate(calibrated)) # 7.777073194074961 float(zcs.npv(calibrated)) # -1666.5413047417533 ``` OIS Pricing [#ois-pricing] Price a 2-year overnight index swap. [OIS](../../api/rates/instruments#ois) is structurally similar to IRS but designed for overnight rate compounding. ```python ois = OIS( effective=effective, termination="2y", frequency="a", fixed_rate=3.90, convention="act360", float_convention="act360", ) float(ois.rate(calibrated)) # 3.8500000000003856 float(ois.npv(calibrated)) # -956.7775507331098 ``` Deposit Pricing [#deposit-pricing] Price a 1-month deposit. [Deposit](../../api/rates/instruments#deposit) `.rate()` returns the implied deposit rate in decimal form. ```python dep = Deposit( effective=effective, termination="1m", rate=0.0440, convention="act360", ) float(dep.rate(calibrated)) # 0.04299999999999926 float(dep.npv(calibrated)) # -83.03578842494552 ``` Cashflow Analysis [#cashflow-analysis] The `.cashflows()` method returns a [Polars DataFrame](../../api/cashflows) with period-level detail for each leg of the instrument. Each row represents one accrual period with its notional, rate, cashflow amount, discount factor, and NPV contribution. ```python irs = IRS( effective=effective, termination="5y", frequency="a", fixed_rate=3.80, convention="act360", float_convention="act360", ) df = irs.cashflows(calibrated) df.columns # ['Type', 'start', 'end', 'payment', 'Ccy', 'Notional', 'fixing_rate', 'Rate', 'Spread', 'DCF', 'Cashflow', 'DF', 'NPV'] df.shape # (14, 13) ``` Next Steps [#next-steps] * [Calibration](../calibration) -- multi-curve calibration and solver algorithms * [Risk](risk) -- delta, gamma, and bucket-level sensitivities # Risk (/docs/guides/rates/risk) Compute delta, gamma, and bucket-level risk sensitivities from calibrated curves using [automatic differentiation](../../getting-started/type-system). The [Solver](../../api/rates/solver#solver) provides AD-based delta and gamma, while [IRImpliedCurve](../../api/rates/curves#irimpliedcurve) offers tenor-bucketed risk reporting. Setup -- Calibrate with Labeled Instruments [#setup----calibrate-with-labeled-instruments] Calibrate a [DiscountCurve](../../api/rates/curves#discountcurve) using the 5-tuple [instrument format](../../api/rates/solver#instruments-format). Labels are required for delta and gamma output to identify which calibrating instrument drives each sensitivity. ```python import datetime from vade import DiscountCurve, IRS, Solver effective = datetime.date(2025, 6, 16) nodes = { effective: 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, datetime.date(2035, 6, 16): 1.0, } curve = DiscountCurve( nodes, interpolation="log_linear", convention="act360", id="sofr" ) irs_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=4.00, convention="act360", float_convention="act360") irs_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") irs_3y = IRS(effective=effective, termination="3Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360") irs_5y = IRS(effective=effective, termination="5Y", frequency="a", fixed_rate=3.70, convention="act360", float_convention="act360") irs_10y = IRS(effective=effective, termination="10Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") instruments = [ (irs_1y, 4.00, 0, "1Y_IRS", "USD"), (irs_2y, 3.85, 0, "2Y_IRS", "USD"), (irs_3y, 3.75, 0, "3Y_IRS", "USD"), (irs_5y, 3.70, 0, "5Y_IRS", "USD"), (irs_10y, 3.85, 0, "10Y_IRS", "USD"), ] solver = Solver(curves=[curve], instruments=instruments) result = solver.iterate() result.converged # True ``` Delta -- First-Order Sensitivities [#delta----first-order-sensitivities] Delta measures how an instrument's NPV changes per unit move in each calibration instrument rate. The solver uses first-order AD (Dual numbers) to compute exact analytic sensitivities without finite differences. ```python import datetime from vade import DiscountCurve, IRS, Solver effective = datetime.date(2025, 6, 16) nodes = { effective: 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, datetime.date(2035, 6, 16): 1.0, } curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360", id="sofr") irs_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=4.00, convention="act360", float_convention="act360") irs_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") irs_3y = IRS(effective=effective, termination="3Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360") irs_5y = IRS(effective=effective, termination="5Y", frequency="a", fixed_rate=3.70, convention="act360", float_convention="act360") irs_10y = IRS(effective=effective, termination="10Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") instruments = [ (irs_1y, 4.00, 0, "1Y_IRS", "USD"), (irs_2y, 3.85, 0, "2Y_IRS", "USD"), (irs_3y, 3.75, 0, "3Y_IRS", "USD"), (irs_5y, 3.70, 0, "5Y_IRS", "USD"), (irs_10y, 3.85, 0, "10Y_IRS", "USD"), ] solver = Solver(curves=[curve], instruments=instruments) result = solver.iterate() # Price a 5Y IRS slightly off-market test_irs = IRS(effective=effective, termination="5Y", frequency="a", fixed_rate=3.80, convention="act360", float_convention="act360") delta_df = solver.delta(test_irs, result) delta_df.columns # ['instrument_label', 'tenor', 'sensitivity', 'currency'] len(delta_df) # 5 any(abs(s) > 1e-10 for s in delta_df["sensitivity"].to_list()) # True ``` Portfolio Delta [#portfolio-delta] Pass a list of instruments to compute aggregated portfolio-level delta. The solver sums sensitivities across all instruments in the list. ```python import datetime from vade import DiscountCurve, IRS, Solver effective = datetime.date(2025, 6, 16) nodes = { effective: 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, datetime.date(2035, 6, 16): 1.0, } curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360", id="sofr") irs_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=4.00, convention="act360", float_convention="act360") irs_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") irs_3y = IRS(effective=effective, termination="3Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360") irs_5y = IRS(effective=effective, termination="5Y", frequency="a", fixed_rate=3.70, convention="act360", float_convention="act360") irs_10y = IRS(effective=effective, termination="10Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") instruments = [ (irs_1y, 4.00, 0, "1Y_IRS", "USD"), (irs_2y, 3.85, 0, "2Y_IRS", "USD"), (irs_3y, 3.75, 0, "3Y_IRS", "USD"), (irs_5y, 3.70, 0, "5Y_IRS", "USD"), (irs_10y, 3.85, 0, "10Y_IRS", "USD"), ] solver = Solver(curves=[curve], instruments=instruments) result = solver.iterate() # Portfolio of two off-market swaps port_irs1 = IRS(effective=effective, termination="3Y", frequency="a", fixed_rate=3.80, convention="act360", float_convention="act360") port_irs2 = IRS(effective=effective, termination="7Y", frequency="a", fixed_rate=3.80, convention="act360", float_convention="act360") portfolio_delta = solver.delta([port_irs1, port_irs2], result) len(portfolio_delta) # 5 portfolio_delta.columns # ['instrument_label', 'tenor', 'sensitivity', 'currency'] ``` Gamma -- Second-Order Sensitivities [#gamma----second-order-sensitivities] Gamma measures second-order (convexity) risk using Dual2 automatic differentiation. The result is a cross-gamma matrix showing how delta changes with respect to each calibrating instrument rate. ```python import datetime from vade import DiscountCurve, IRS, Solver effective = datetime.date(2025, 6, 16) nodes = { effective: 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, datetime.date(2035, 6, 16): 1.0, } curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360", id="sofr") irs_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=4.00, convention="act360", float_convention="act360") irs_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") irs_3y = IRS(effective=effective, termination="3Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360") irs_5y = IRS(effective=effective, termination="5Y", frequency="a", fixed_rate=3.70, convention="act360", float_convention="act360") irs_10y = IRS(effective=effective, termination="10Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") instruments = [ (irs_1y, 4.00, 0, "1Y_IRS", "USD"), (irs_2y, 3.85, 0, "2Y_IRS", "USD"), (irs_3y, 3.75, 0, "3Y_IRS", "USD"), (irs_5y, 3.70, 0, "5Y_IRS", "USD"), (irs_10y, 3.85, 0, "10Y_IRS", "USD"), ] solver = Solver(curves=[curve], instruments=instruments) result = solver.iterate() test_irs = IRS(effective=effective, termination="5Y", frequency="a", fixed_rate=3.80, convention="act360", float_convention="act360") gamma_df = solver.gamma(test_irs, result) "label" in gamma_df.columns # True len(gamma_df) # 5 ``` Bucket-Level Risk [#bucket-level-risk] [IRImpliedCurve](../../api/rates/curves#irimpliedcurve) provides tenor-bucketed DV01 sensitivities by re-parameterizing a calibrated curve into standard tenor buckets. This gives a risk report aligned with market-standard tenor points. ```python import datetime import math from vade import DiscountCurve, IRImpliedCurve, BusinessCalendar, FixedRateBond effective = datetime.date(2024, 1, 1) # Build a curve with known DFs for bucket risk nodes = {effective: 1.0} for y in range(1, 11): nodes[datetime.date(2024 + y, 1, 1)] = math.exp(-0.03 * y) source = DiscountCurve( nodes, interpolation="log_linear", convention="act365f", id="test_curve" ) cal = BusinessCalendar("NYC") tenors = ["1Y", "2Y", "5Y", "10Y"] implied = IRImpliedCurve(source, effective, tenors, cal) implied.tenor_labels() # ['0D', '1Y', '2Y', '5Y', '10Y'] float(implied.discount_factor(datetime.date(2025, 1, 1))) > 0.95 # True bond = FixedRateBond(effective=effective, termination="5Y", coupon=0.04, frequency="s") bucket_df = implied.bucket_delta(bond) "tenor" in bucket_df.columns # True "delta" in bucket_df.columns # True any(abs(float(d)) > 1e-12 for d in bucket_df["delta"].to_list()) # True ``` AAD Delta vs Analytical DV01 [#aad-delta-vs-analytical-dv01] The [Solver](../../api/rates/solver#solver) and [FixedRateBond](../../api/credit/instruments#fixedratebond) offer two different sensitivity measures that answer different questions: * **`Solver.delta(instrument, result)`** computes dNPV/dr\_i via automatic differentiation, where r\_i is each calibrating instrument rate. It answers: *"how does this instrument's NPV change per unit move in each curve-calibrating rate?"* The output is a per-instrument breakdown of curve risk. * **`bond.dv01(curves)`** computes -dP/dy \* 0.0001 via analytical formula. It answers: *"how does the bond's price change per 1 basis point parallel shift in yield?"* This is a single scalar measuring price-level yield sensitivity. **The \~100x scaling difference with QuantLib:** `Solver.delta()` returns sensitivity per 1.0 rate move (units: NPV change per 100% rate shift). QuantLib's `BondFunctions::basisPointValue` reports at the price level per 100 face. On a bond with 100 face value, Vade's `Solver.delta()` on the dominant bucket gives roughly -4.5, while QuantLib reports roughly -445 -- the factor is notional/price scaling and the per-unit vs per-100bp convention. To compare directly, use `bond.dv01()` which matches QuantLib's `BondFunctions::basisPointValue` in magnitude. | Method | What it computes | When to use | QuantLib equivalent | | ------------------ | -------------------------------------- | ----------------------------- | -------------------------------------------------------- | | `Solver.delta()` | AAD dNPV/dr per calibrating instrument | Curve risk, hedging ratios | BondFunctions::basisPointValue (but AD, not finite-diff) | | `bond.dv01()` | Analytical -dP/dy per bp | Quick bond risk, P\&L explain | BondFunctions::basisPointValue | | `bond.duration()` | Curve-based modified/macaulay duration | Duration matching | BondFunctions::duration | | `bond.convexity()` | Curve-based bond convexity | Convexity adjustment | BondFunctions::convexity | The following example builds a 5-year bond on the same curve used above and compares both measures: ```python import datetime from vade import DiscountCurve, IRS, Solver, FixedRateBond effective = datetime.date(2025, 6, 16) nodes = { effective: 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, datetime.date(2035, 6, 16): 1.0, } curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360", id="sofr") irs_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=4.00, convention="act360", float_convention="act360") irs_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") irs_3y = IRS(effective=effective, termination="3Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360") irs_5y = IRS(effective=effective, termination="5Y", frequency="a", fixed_rate=3.70, convention="act360", float_convention="act360") irs_10y = IRS(effective=effective, termination="10Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") instruments = [ (irs_1y, 4.00, 0, "1Y_IRS", "USD"), (irs_2y, 3.85, 0, "2Y_IRS", "USD"), (irs_3y, 3.75, 0, "3Y_IRS", "USD"), (irs_5y, 3.70, 0, "5Y_IRS", "USD"), (irs_10y, 3.85, 0, "10Y_IRS", "USD"), ] solver = Solver(curves=[curve], instruments=instruments) result = solver.iterate() assert result.converged calibrated = solver.get_curve(0) bond = FixedRateBond( effective=effective, termination="5Y", coupon=0.04, frequency="s", convention="act360", face_value=100.0, ) # AAD delta: per-instrument curve sensitivity (dNPV/dr_i) delta_df = solver.delta(bond, result) assert delta_df.columns == ["instrument_label", "tenor", "sensitivity", "currency"] dominant = max(abs(s) for s in delta_df["sensitivity"].to_list()) assert dominant > 1.0 # largest bucket sensitivity is ~4.5 # Analytical DV01: price change per 1bp yield shift dv01 = bond.dv01(calibrated) assert 0.01 < dv01 < 1.0 # ~0.046 for a 5Y bond with 100 face # Modified duration: percentage price change per 1% yield shift duration = bond.duration(calibrated) assert 3.0 < duration < 6.0 # ~4.5 for a 5Y semi-annual bond ``` Gamma vs Convexity [#gamma-vs-convexity] `Solver.gamma()` and `bond.convexity()` measure different second-order effects: * **`Solver.gamma(instrument, result)`** computes the cross-instrument gamma matrix d2NPV/(dr\_i \* dr\_j) -- how delta with respect to one calibrating instrument changes as another calibrating rate moves. This is a matrix of curve-level second derivatives, useful for understanding non-linear curve risk and gamma hedging across tenors. * **`bond.convexity(curves)`** computes d2P/dy2, the bond's price-yield convexity -- how the bond's duration itself changes as yields move. This is the standard bond convexity used in duration-convexity approximations. These are entirely different quantities. QuantLib's `BondFunctions::convexity` returns bond convexity (e.g., \~23.6 for a 5Y bond), while Vade's `Solver.gamma()` returns cross-instrument second derivatives (values near zero like -0.003). Comparing them directly is meaningless. **Use `Solver.gamma()` for:** cross-tenor gamma risk, gamma hedging portfolios, understanding non-linear curve exposure. **Use `bond.convexity()` for:** duration-convexity price approximation, bond relative value, convexity-adjusted hedging. ```python import datetime from vade import DiscountCurve, IRS, Solver, FixedRateBond effective = datetime.date(2025, 6, 16) nodes = { effective: 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0, datetime.date(2028, 6, 16): 1.0, datetime.date(2030, 6, 16): 1.0, datetime.date(2035, 6, 16): 1.0, } curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360", id="sofr") irs_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=4.00, convention="act360", float_convention="act360") irs_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") irs_3y = IRS(effective=effective, termination="3Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360") irs_5y = IRS(effective=effective, termination="5Y", frequency="a", fixed_rate=3.70, convention="act360", float_convention="act360") irs_10y = IRS(effective=effective, termination="10Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360") instruments = [ (irs_1y, 4.00, 0, "1Y_IRS", "USD"), (irs_2y, 3.85, 0, "2Y_IRS", "USD"), (irs_3y, 3.75, 0, "3Y_IRS", "USD"), (irs_5y, 3.70, 0, "5Y_IRS", "USD"), (irs_10y, 3.85, 0, "10Y_IRS", "USD"), ] solver = Solver(curves=[curve], instruments=instruments) result = solver.iterate() assert result.converged calibrated = solver.get_curve(0) bond = FixedRateBond( effective=effective, termination="5Y", coupon=0.04, frequency="s", convention="act360", face_value=100.0, ) # Cross-instrument gamma: d2NPV/(dr_i * dr_j) matrix gamma_df = solver.gamma(bond, result) assert "label" in gamma_df.columns assert len(gamma_df) == 5 # one row per calibrating instrument # Bond convexity: d2P/dy2 (standard bond convexity) convexity = bond.convexity(calibrated) assert convexity > 10.0 # ~23.6 for a 5Y semi-annual bond # These are fundamentally different: gamma values are near zero (cross-derivatives), # while convexity is a much larger number (price-yield curvature). gamma_values = [gamma_df["5Y_IRS"][i] for i in range(len(gamma_df))] assert max(abs(v) for v in gamma_values) < 1.0 # cross-gamma values are small assert convexity > 10 * max(abs(v) for v in gamma_values) # convexity >> gamma ``` Next Steps [#next-steps] * [Calibration](../calibration) -- multi-curve frameworks and solver algorithm comparison * [Curve Building](curve-building) -- interpolation methods and curve types