Esc

    The API is under testing and may change before release.

    Equity option

    This page lists what you need to price a European equity option, with or without a single barrier: the constructor's arguments, the market it reads, how it's priced, and an example you can run. An equity option is the right to buy (a call) or sell (a put) a number of shares at a strike on its expiry date.

    Quick example

    The example reads its dollar quotes from the desk sheets; load them once and it runs as shown.

    This marks a dollar curve, an ACME spot of 120 and a vol surface over two expiries and three strikes. It prices a one-year at-the-money call and put on a hundred shares and checks put-call parity. Then it puts the call behind a down barrier at 100, as a knock-out and as a knock-in, tries a barrier the spot has already crossed, and takes the short side.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE};
    use helixrs::{
        AnyInstrument, BarrierKind, CollateralSpec, Currency, EquityOption, FutureOptionVolSurface,
        MarketContext, OptionKind, QuoteSheet, VolConvention, YieldCurve, black76,
    };
    
    type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
    
    const SPOT: f64 = 120.0;
    const STRIKE: f64 = 120.0;
    const SHARES: f64 = 100.0;
    
    fn date((year, month, day): (i32, u32, u32)) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
    }
    
    /// An option on `quantity` ACME shares struck at the money for a year.
    fn option(usd: Currency, kind: OptionKind, quantity: f64) -> Outcome<EquityOption> {
        Ok(EquityOption::new(
            "ACME",
            usd,
            kind,
            STRIKE,
            date((2027, 10, 1)),
            quantity,
        )?)
    }
    
    fn boxed(option: EquityOption) -> AnyInstrument {
        AnyInstrument::EquityOption(Box::new(option))
    }
    
    fn main() -> Outcome<()> {
        let close = date(SOFR_CLOSE_DATE);
        let expiry = date((2027, 10, 1));
        let usd: Currency = "USD".parse()?;
        let surface = FutureOptionVolSurface::new(
            close,
            vec![date((2027, 4, 1)), expiry],
            vec![100.0, 120.0, 140.0],
            &[vec![0.29, 0.26, 0.24], vec![0.28, 0.25, 0.235]],
            VolConvention::Lognormal,
        )?;
        let market = MarketContext::as_of(close)
            .with_recipe_quotes(
                &CollateralSpec::Currency(usd),
                &QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?,
            )?
            .with_equity_spot("ACME", SPOT)?
            .with_equity_vols("ACME", surface.clone());
    
        // A one-year at-the-money call and put on a hundred shares.
        let call = boxed(option(usd, OptionKind::Call, SHARES)?).npv(&market)?;
        let put = boxed(option(usd, OptionKind::Put, SHARES)?).npv(&market)?;
        let (curve, _) = market
            .curve(&"discount(USD)".parse()?)
            .expect("the USD discount curve");
        let df = curve.view::<f64>().discount(expiry)?;
        let parity = SHARES * (SPOT - STRIKE * df);
        println!("call        {call:.6}");
        println!("put         {put:.6}");
        println!("call - put  {:.6}", call - put);
        println!("S - K P(T)  {parity:.6}");
    
        // A down-and-out call at 100 dies if ACME trades down through 100; the
        // down-and-in call at 100 only comes alive if it does. Between them they are
        // the vanilla call, so the two add up to it.
        let barred = |kind: BarrierKind, level: f64| -> Outcome<AnyInstrument> {
            Ok(boxed(
                option(usd, OptionKind::Call, SHARES)?.with_barrier(kind, level, 0.0)?,
            ))
        };
        let knock_out = barred(BarrierKind::DownOut, 100.0)?.npv(&market)?;
        let knock_in = barred(BarrierKind::DownIn, 100.0)?.npv(&market)?;
        println!("down-out    {knock_out:.6}");
        println!("down-in     {knock_in:.6}");
        println!("sum         {:.6}", knock_out + knock_in);
        // A barrier the spot has already crossed is refused: a down barrier above 120.
        let refused = barred(BarrierKind::DownOut, 130.0)?.npv(&market).is_err();
        println!(
            "spot through the barrier refused  {}",
            if refused { "yes" } else { "no" }
        );
    
        // A negative quantity is the short side: the same premium with the sign turned.
        let short = boxed(option(usd, OptionKind::Call, -SHARES)?).npv(&market)?;
        println!("short call  {short:.6}");
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
    
    SPOT = 120.0
    EXPIRY = dt.date(2027, 10, 1)
    STRIKE = 120.0
    SHARES = 100.0
    surface = hlx.vols.FutureOptionVolSurface(
        SOFR_CLOSE_DATE,
        [dt.date(2027, 4, 1), EXPIRY],
        [100.0, 120.0, 140.0],
        [[0.29, 0.26, 0.24], [0.28, 0.25, 0.235]],
        "lognormal",
    )
    market = (
        hlx.MarketContext(SOFR_CLOSE_DATE)
        .with_recipe_quotes("USD", USD_SOFR_CLOSE)
        .with_equity_spot("ACME", SPOT)
        .with_equity_vols("ACME", surface)
    )
    
    def option(kind, **kw):
        """An option on `SHARES` ACME shares struck at the money for a year."""
        return hlx.products.equity_option("ACME", "USD", strike=STRIKE, expiry=EXPIRY, kind=kind, quantity=SHARES, **kw)
    
    # A one-year at-the-money call and put on a hundred shares.
    call, put = option("call").npv(market), option("put").npv(market)
    df = market.discount_factor("USD", EXPIRY)
    parity = SHARES * (SPOT - STRIKE * df)
    print(f"call        {call:.6f}")
    print(f"put         {put:.6f}")
    print(f"call - put  {call - put:.6f}")
    print(f"S - K P(T)  {parity:.6f}")
    
    # A down-and-out call at 100 dies if ACME trades down through 100; the
    # down-and-in call at 100 only comes alive if it does. Between them they are
    # the vanilla call, so the two add up to it.
    knock_out = option("call", barrier=100.0, barrier_kind="down_out").npv(market)
    knock_in = option("call", barrier=100.0, barrier_kind="down_in").npv(market)
    print(f"down-out    {knock_out:.6f}")
    print(f"down-in     {knock_in:.6f}")
    print(f"sum         {knock_out + knock_in:.6f}")
    # A barrier the spot has already crossed is refused: a down barrier above 120.
    try:
        option("call", barrier=130.0, barrier_kind="down_out").npv(market)
        refused = False
    except Exception:
        refused = True
    print(f"spot through the barrier refused  {'yes' if refused else 'no'}")
    
    # A negative quantity is the short side: the same premium with the sign turned.
    short = hlx.products.equity_option("ACME", "USD", strike=STRIKE, expiry=EXPIRY, kind="call", quantity=-SHARES)
    print(f"short call  {short.npv(market):.6f}")
    call        1441.218272
    put         930.492266
    call - put  510.726006
    S - K P(T)  510.726006
    down-out    1334.657614
    down-in     106.560658
    sum         1441.218272
    spot through the barrier refused  yes
    short call  -1441.218272

    The call less the put is the spot less the strike discounted, to the sixth decimal. Each barrier option is cheaper than the vanilla, and the knock-out and knock-in add back up to it exactly. A down barrier at 130, above the spot, is already crossed, so it's refused. The short call is the long call with the sign turned.

    How it's priced

    With no dividends the forward to expiry TT is F=S/P(T)F = S / P(T), and a call on QQ shares struck at KK is Black-76 on that forward:

    C=Q P(T) Black(F,K,σ,τ),C = Q\,P(T)\,\mathrm{Black}(F, K, \sigma, \tau),

    with σ\sigma the surface's vol at (T,K)(T, K) and τ\tau the surface's ACT/365 Fixed time to expiry. Put-call parity, and why a knock-in and a knock-out add up to the vanilla, are covered in how equity products work.

    API

    Argument Type Default Meaning
    products.equity_option(name, currency, ...) str, str required the share and the trade currency; Rust EquityOption::new(name, currency, kind, strike, expiry, quantity)
    strike float required the strike, per share
    expiry date required the day the option expires and pays
    kind "call" or "put" required Rust OptionKind
    quantity float 1.0 the number of shares; negative is the short side
    barrier float None a barrier level; give it with barrier_kind
    barrier_kind "down_in", "down_out", "up_in" or "up_out" None where the barrier sits and what touching it does; Rust BarrierKind via EquityOption::with_barrier(kind, level, rebate)
    rebate float 0.0 paid per share to a knocked-out holder, or to a knock-in that never knocked in
    Symbol Meaning Rust Python
    the trade a call or put on quantity shares EquityOption::new(…) in AnyInstrument::EquityOption products.equity_option(…)
    SS, σ\sigma the spot and the surface MarketContext::with_equity_spot, with_equity_vols MarketContext.with_equity_spot, with_equity_vols
    P(T)P(T) the discount factor to expiry curve.view::<f64>().discount(T) MarketContext.discount_factor
    Black\mathrm{Black} Black-76 on the forward black76(forward, strike, vol, expiry, kind) models.black76(forward, strike, vol, expiry, kind)
    CC, PP the premium AnyInstrument::npv Instrument.npv

    Defaults and limits

    • The market: the trade currency's discount curve, the share's spot from with_equity_spot, and its surface from with_equity_vols. The surface is a FutureOptionVolSurface of expiries by strikes, and must be lognormal; a normal surface is refused.
    • Expiry: on and after expiry the option is worth 0.
    • Exercise: European only; the option expires and pays on expiry.
    • Dividends: the model assumes a dividend-free spot. A share paying a dividend before expiry has a lower forward than S/P(T)S / P(T), and this option doesn't see it.
    • Barriers: one barrier, monitored continuously. The spot must be on the live side of it: a spot already through the barrier is refused when the option is valued.
    • Currency of the value: the npv is in the trade currency.

    See also

    • Source: crates/helixrs/src/instruments/equity.rs, with the barrier formulas in crates/helixrs/src/models/analytic.rs
    • Related pages:
      • How equity products work, the ideas behind the price, and barriers for the knock-in and knock-out.
      • Equity spots and vols, for the spot and surface the option reads.
      • Equity total return swap, for the share's return as a swap.
      • FX options, for the same Black-76 on a currency forward.

    From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.