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.
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(())
}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 is , and a call on shares struck at is Black-76 on that forward:
with the surface's vol at and 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(…) |
| , | the spot and the surface | MarketContext::with_equity_spot, with_equity_vols |
MarketContext.with_equity_spot, with_equity_vols |
| the discount factor to expiry | curve.view::<f64>().discount(T) |
MarketContext.discount_factor |
|
| Black-76 on the forward | black76(forward, strike, vol, expiry, kind) |
models.black76(forward, strike, vol, expiry, kind) |
|
| , | 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 fromwith_equity_vols. The surface is aFutureOptionVolSurfaceof expiries by strikes, and must be lognormal; a normal surface is refused. - Expiry: on and after
expirythe 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 , 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 incrates/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.