Skip to content

Getting started

The same pricing task in Python, Rust, and Go. All three use the Rust numerical core. The snippets below include the real runnable files from the repository, so the documentation stays in sync with the examples.

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());
}
sdk/go/examples/european_option/main.go
package main

import (
    "errors"
    "fmt"
    "log"

    itofin "github.com/benbenbang/libitofin/sdk/go"
)

func main() {
    if err := run(); err != nil {
        log.Fatal(err)
    }
}

func run() (err error) {
    session, err := itofin.NewSession()
    if err != nil {
        return err
    }
    defer func() { err = errors.Join(err, session.Close()) }()

    today, err := itofin.NewDate(15, 6, 2026)
    if err != nil {
        return err
    }
    expiry, err := today.AddDays(90)
    if err != nil {
        return err
    }
    settings, err := session.NewSettings()
    if err != nil {
        return err
    }
    if err = settings.SetEvaluationDate(today); err != nil {
        return err
    }
    dayCounter, err := session.Actual360()
    if err != nil {
        return err
    }
    process, err := session.NewBlackScholesProcess(itofin.BlackScholesConfig{
        Spot:          60,
        RiskFreeRate:  0.08,
        DividendYield: 0,
        Volatility:    0.30,
        ReferenceDate: today,
        DayCounter:    dayCounter,
    })
    if err != nil {
        return err
    }
    option, err := session.NewVanillaOption(itofin.Call, 65, expiry, settings)
    if err != nil {
        return err
    }
    if err = option.SetEngine(process); err != nil {
        return err
    }

    fmt.Println("European call, K=65, 90d, spot=60, vol=30%, r=8%")
    for _, result := range []struct {
        name  string
        value func() (float64, error)
    }{
        {"NPV", option.NPV},
        {"delta", option.Delta},
        {"gamma", option.Gamma},
        {"theta", option.Theta},
        {"vega", option.Vega},
        {"rho", option.Rho},
        {"dividend_rho", option.DividendRho},
    } {
        value, err := result.value()
        if err != nil {
            return fmt.Errorf("%s: %w", result.name, err)
        }
        fmt.Printf("  %-12s = %.10f\n", result.name, value)
    }
    return nil
}

Run them from a source checkout. For Go, first follow the native build and runtime setup. For an application using the published module, follow Go installation.

python example/python/european_option.py
cargo run --example european_option
cd sdk/go
go run ./examples/european_option

The Python and Go examples price the same call: spot 60, strike 65, 90 days to expiry, 30% volatility, 8% risk-free rate, zero dividends, and Actual/360 day count. Their expected NPV is 2.1333684449. The Rust example illustrates the same pricing workflow with a different market and maturity.

More worked examples

Go examples currently cover the European option above and correlated portfolio simulation. The Python and Rust examples below cover additional products:

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