Skip to content

Getting started

The same pricing task in Python and in Rust. The Python API is the thin binding; the Rust core is where the numerics live. Both snippets below are the real, runnable example files from the repository (example/python and crates/libitofin/examples), included verbatim, so what you read here is exactly what runs.

Price a European option

Build a Black-Scholes market, wrap it in a vanilla option, attach the analytic engine and read the value plus the greeks.

example/python/european_option.py
"""Price a European option and read its greeks.

This is the "hello world" of itofin: build a Black-Scholes market, wrap it in a
`VanillaOption`, attach the analytic engine and read the value plus every greek.

The numbers below reproduce the pricing gate in
`crates/itofin-py/tests/test_european_option.py` (row 1), so the printed NPV of
2.1333684449161985 is a self-check: if it drifts, the market was wired wrong.

Run it with:

    python example/python/european_option.py
"""

# plugins
# itofin library
from itofin import Settings
from itofin.instruments import OptionType, VanillaOption
from itofin.processes import BlackScholesProcess
from itofin.time import Date, DayCounter


def main() -> None:
    # D5: one Settings object holds the evaluation date and is threaded into
    # every instrument. There is no global "today"; an unset date is an error,
    # not a silent fall back to the system clock.
    settings = Settings()
    today = Date(15, 6, 2026)
    settings.set_evaluation_date(today)

    # A generalized Black-Scholes process from scalar market data:
    #   spot 60, risk-free 8%, dividend yield 0%, volatility 30%,
    # all quoted on an Actual/360 day count as of `today`.
    day_counter = DayCounter.actual360()
    process = BlackScholesProcess(
        60.0,  # spot
        0.08,  # risk-free rate
        0.0,  # dividend yield
        0.30,  # volatility
        today,  # reference date
        day_counter,
    )

    # A European call struck at 65, expiring 90 calendar days out.
    # `Date + int` advances by days (not by a Period).
    option = VanillaOption(OptionType.Call, 65.0, today + 90, settings)

    # Attaching the process installs the analytic European engine.
    option.set_engine(process)

    # NPV plus the full greek set. Each read reprices lazily off the process.
    print("European call, K=65, 90d, spot=60, vol=30%, r=8%")
    print(f"  NPV          = {option.npv():.10f}")
    print(f"  delta        = {option.delta():.10f}")
    print(f"  gamma        = {option.gamma():.10f}")
    print(f"  theta        = {option.theta():.10f}")
    print(f"  vega         = {option.vega():.10f}")
    print(f"  rho          = {option.rho():.10f}")
    print(f"  dividend_rho = {option.dividend_rho():.10f}")


if __name__ == "__main__":
    main()
crates/libitofin/examples/european_option.rs
//! Price a European option with the AnalyticEuropeanEngine and print
//! NPV + greeks (delta, gamma, vega, theta, rho).
//!
//! Mirrors the working `mixed_day_counters` M1-slice test in
//! `crates/libitofin/src/pricingengines/vanilla/mod.rs`. Every construction
//! step is the same one the crate's own tests exercise.

use libitofin::exercise::EuropeanExercise;
use libitofin::handle::Handle;
use libitofin::instrument::Instrument; // brings `npv`, `base_mut` into scope
use libitofin::instruments::{EuropeanOption, PlainVanillaPayoff};
use libitofin::interestrate::Compounding;
use libitofin::option::OptionType;
use libitofin::pricingengine::PricingEngine;
use libitofin::pricingengines::AnalyticEuropeanEngine;
use libitofin::processes::BlackScholesMertonProcess;
use libitofin::quotes::{Quote, SimpleQuote};
use libitofin::settings::Settings;
use libitofin::shared::{Shared, SharedMut, shared, shared_mut};
use libitofin::termstructures::volatility::{BlackConstantVol, BlackVolTermStructure};
use libitofin::termstructures::yields::FlatForward;
use libitofin::termstructures::yieldtermstructure::YieldTermStructure;
use libitofin::time::date::{Date, Month};
use libitofin::time::daycounters::actual360::Actual360;
use libitofin::time::frequency::Frequency;
use libitofin::types::Real;

fn main() {
    // --- Market date. D5: the evaluation date is set explicitly on an owned
    // Settings, not read from a global clock. ---
    let today = Date::new(15, Month::June, 2026);
    let expiry = today + 146; // Date + i64 shifts by calendar days

    let spot: Real = 100.0;
    let strike: Real = 105.0;
    let q_rate: Real = 0.04; // continuous dividend yield
    let r_rate: Real = 0.06; // continuous risk-free rate
    let vol: Real = 0.20; // flat Black vol

    // Settings is shared (Rc) so the option can register against its
    // evaluation-date observable and recompute if the date changes.
    let settings = shared(Settings::new());
    settings.set_evaluation_date(today);

    let dc = Actual360::new(); // DayCounter used for all three curves

    // --- Black-Scholes-Merton process: spot quote + dividend curve +
    // risk-free curve + Black vol surface, each wrapped in a Handle so it
    // can be relinked live. The engine discounts on the risk-free curve
    // embedded here. ---
    let process = shared(BlackScholesMertonProcess::new(
        Handle::new(shared(SimpleQuote::new(spot)) as Shared<dyn Quote>),
        Handle::new(shared(FlatForward::with_rate(
            today,
            q_rate,
            dc.clone(),
            Compounding::Continuous,
            Frequency::Annual,
        )) as Shared<dyn YieldTermStructure>),
        Handle::new(shared(FlatForward::with_rate(
            today,
            r_rate,
            dc.clone(),
            Compounding::Continuous,
            Frequency::Annual,
        )) as Shared<dyn YieldTermStructure>),
        Handle::new(shared(BlackConstantVol::new(today, None, vol, dc.clone()))
            as Shared<dyn BlackVolTermStructure>),
    ));

    // --- Instrument: a plain-vanilla call struck at 105, European exercise. ---
    let payoff = shared(PlainVanillaPayoff::new(OptionType::Call, strike));
    let exercise = shared(EuropeanExercise::new(expiry));
    let mut option = EuropeanOption::new(payoff, exercise, Shared::clone(&settings));

    // --- Attach the analytic engine. It is a SharedMut (Rc<RefCell<..>>)
    // because pricing mutates the engine's cached results. `base_mut()` and
    // `set_pricing_engine` come from the Instrument trait / InstrumentBase. ---
    let engine = shared_mut(AnalyticEuropeanEngine::new(Shared::clone(&process)));
    option
        .base_mut()
        .set_pricing_engine(engine as SharedMut<dyn PricingEngine>);

    // --- Results. Each accessor triggers `calculate()` lazily and returns a
    // Result (D4: explicit errors, e.g. if no engine/exercise were set). ---
    println!("NPV   = {:.6}", option.npv().unwrap());
    println!("delta = {:.6}", option.delta().unwrap());
    println!("gamma = {:.6}", option.gamma().unwrap());
    println!("vega  = {:.6}", option.vega().unwrap());
    println!("theta = {:.6}", option.theta().unwrap());
    println!("rho   = {:.6}", option.rho().unwrap());
}

Run them with:

python example/python/european_option.py
cargo run --example european_option

More worked examples

Every example ships in both languages with a matching filename. Browse the full set:

Topic Python Rust
European option european_option.py european_option.rs
Monte Carlo monte_carlo.py monte_carlo.rs
Vanilla swap vanilla_swap.py vanilla_swap.rs
Yield curve yield_curve.py yield_curve.rs
Credit CDS credit_cds.py credit_cds.rs
ISDA CDS isda_cds.py isda_cds.rs
Inflation swap inflation_swap.py inflation_swap.rs
YoY inflation cap/floor yoy_inflation_capfloor.py yoy_inflation_capfloor.rs