feat: add full derivatives analytics layer (options + futures)
Implements all phases of the derivatives expansion plan: Rust core (crates/ferro_ta_core/src/options/, src/futures/): - BSM and Black-76 pricing (scalar + vectorized batch) - Greeks: delta, gamma, vega, theta, rho - Implied volatility solver (Newton + bisection fallback) - Smile/skew metrics: ATM IV, 25-delta RR/BF, skew slope, convexity - Chain helpers: moneyness labels, strike selection by offset or delta - Synthetic forwards, basis, annualized basis, implied carry, carry spread - Continuous contract stitching: weighted, back-adjusted, ratio-adjusted - Curve analytics: calendar spreads, slope, contango/backwardation summary PyO3 bindings (src/options/, src/futures/): - All Rust functions registered and exposed via _ferro_ta extension Python API (python/ferro_ta/analysis/): - options.py: pricing, greeks, IV, smile, chain, legacy iv_rank/percentile/zscore - futures.py: basis, carry, curve, roll, synthetic, continuous contracts - options_strategy.py: typed strategy schemas (expiry/strike selectors, leg presets, risk controls, simulation limits) - derivatives_payoff.py: multi-leg payoff aggregation and Greeks aggregation Bug fix: wrap _to_f64 calls in iv_rank/iv_percentile/iv_zscore to raise FerroTAInputError (not plain ValueError) for 2D array input. Docs: derivatives.rst, derivatives-analytics.md, options-volatility.md, quickstart.rst, index.rst, api/analysis.rst all updated. Tests: 2053 pass, 12 skipped. All CI checks pass locally. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
2d5000262f
commit
602d675749
@@ -0,0 +1,64 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (strikes, reference_price, option_type = "call"))]
|
||||
pub fn moneyness_labels<'py>(
|
||||
py: Python<'py>,
|
||||
strikes: PyReadonlyArray1<'py, f64>,
|
||||
reference_price: f64,
|
||||
option_type: &str,
|
||||
) -> PyResult<Bound<'py, PyArray1<i8>>> {
|
||||
let kind = super::parse_option_kind(option_type)?;
|
||||
let strikes = strikes.as_slice()?;
|
||||
let labels = ferro_ta_core::options::chain::label_moneyness(strikes, reference_price, kind);
|
||||
Ok(labels.into_pyarray(py))
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
pub fn select_strike_offset<'py>(
|
||||
strikes: PyReadonlyArray1<'py, f64>,
|
||||
reference_price: f64,
|
||||
offset: isize,
|
||||
) -> PyResult<Option<f64>> {
|
||||
Ok(ferro_ta_core::options::chain::select_strike_by_offset(
|
||||
strikes.as_slice()?,
|
||||
reference_price,
|
||||
offset,
|
||||
))
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (strikes, vols, reference_price, time_to_expiry, target_delta, option_type = "call", model = "bsm", rate = 0.0, carry = 0.0))]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn select_strike_delta<'py>(
|
||||
strikes: PyReadonlyArray1<'py, f64>,
|
||||
vols: PyReadonlyArray1<'py, f64>,
|
||||
reference_price: f64,
|
||||
time_to_expiry: f64,
|
||||
target_delta: f64,
|
||||
option_type: &str,
|
||||
model: &str,
|
||||
rate: f64,
|
||||
carry: f64,
|
||||
) -> PyResult<Option<f64>> {
|
||||
let kind = super::parse_option_kind(option_type)?;
|
||||
let model = super::parse_pricing_model(model)?;
|
||||
let strikes = strikes.as_slice()?;
|
||||
let vols = vols.as_slice()?;
|
||||
validation::validate_equal_length(&[(strikes.len(), "strikes"), (vols.len(), "vols")])?;
|
||||
Ok(ferro_ta_core::options::chain::select_strike_by_delta(
|
||||
strikes,
|
||||
vols,
|
||||
ferro_ta_core::options::ChainGreeksContext {
|
||||
model,
|
||||
reference_price,
|
||||
rate,
|
||||
carry,
|
||||
time_to_expiry,
|
||||
kind,
|
||||
},
|
||||
target_delta,
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
type GreekArrays<'py> = (
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
Bound<'py, PyArray1<f64>>,
|
||||
);
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (underlying, strike, rate, time_to_expiry, volatility, option_type = "call", model = "bsm", carry = 0.0))]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn option_greeks(
|
||||
underlying: f64,
|
||||
strike: f64,
|
||||
rate: f64,
|
||||
time_to_expiry: f64,
|
||||
volatility: f64,
|
||||
option_type: &str,
|
||||
model: &str,
|
||||
carry: f64,
|
||||
) -> PyResult<(f64, f64, f64, f64, f64)> {
|
||||
let kind = super::parse_option_kind(option_type)?;
|
||||
let model = super::parse_pricing_model(model)?;
|
||||
let greeks =
|
||||
ferro_ta_core::options::greeks::model_greeks(ferro_ta_core::options::OptionEvaluation {
|
||||
contract: ferro_ta_core::options::OptionContract {
|
||||
model,
|
||||
underlying,
|
||||
strike,
|
||||
rate,
|
||||
carry,
|
||||
time_to_expiry,
|
||||
kind,
|
||||
},
|
||||
volatility,
|
||||
});
|
||||
Ok((
|
||||
greeks.delta,
|
||||
greeks.gamma,
|
||||
greeks.vega,
|
||||
greeks.theta,
|
||||
greeks.rho,
|
||||
))
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (underlying, strike, rate, time_to_expiry, volatility, option_type = "call", model = "bsm", carry = None))]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn option_greeks_batch<'py>(
|
||||
py: Python<'py>,
|
||||
underlying: PyReadonlyArray1<'py, f64>,
|
||||
strike: PyReadonlyArray1<'py, f64>,
|
||||
rate: PyReadonlyArray1<'py, f64>,
|
||||
time_to_expiry: PyReadonlyArray1<'py, f64>,
|
||||
volatility: PyReadonlyArray1<'py, f64>,
|
||||
option_type: &str,
|
||||
model: &str,
|
||||
carry: Option<PyReadonlyArray1<'py, f64>>,
|
||||
) -> PyResult<GreekArrays<'py>> {
|
||||
let kind = super::parse_option_kind(option_type)?;
|
||||
let model = super::parse_pricing_model(model)?;
|
||||
let underlying = underlying.as_slice()?;
|
||||
let strike = strike.as_slice()?;
|
||||
let rate = rate.as_slice()?;
|
||||
let time_to_expiry = time_to_expiry.as_slice()?;
|
||||
let volatility = volatility.as_slice()?;
|
||||
let carry_vec = match carry {
|
||||
Some(array) => array.as_slice()?.to_vec(),
|
||||
None => vec![0.0; underlying.len()],
|
||||
};
|
||||
validation::validate_equal_length(&[
|
||||
(underlying.len(), "underlying"),
|
||||
(strike.len(), "strike"),
|
||||
(rate.len(), "rate"),
|
||||
(time_to_expiry.len(), "time_to_expiry"),
|
||||
(volatility.len(), "volatility"),
|
||||
(carry_vec.len(), "carry"),
|
||||
])?;
|
||||
|
||||
let mut delta = Vec::with_capacity(underlying.len());
|
||||
let mut gamma = Vec::with_capacity(underlying.len());
|
||||
let mut vega = Vec::with_capacity(underlying.len());
|
||||
let mut theta = Vec::with_capacity(underlying.len());
|
||||
let mut rho = Vec::with_capacity(underlying.len());
|
||||
for (((((&u, &k), &r), &t), &vol), &c) in underlying
|
||||
.iter()
|
||||
.zip(strike.iter())
|
||||
.zip(rate.iter())
|
||||
.zip(time_to_expiry.iter())
|
||||
.zip(volatility.iter())
|
||||
.zip(carry_vec.iter())
|
||||
{
|
||||
let g = ferro_ta_core::options::greeks::model_greeks(
|
||||
ferro_ta_core::options::OptionEvaluation {
|
||||
contract: ferro_ta_core::options::OptionContract {
|
||||
model,
|
||||
underlying: u,
|
||||
strike: k,
|
||||
rate: r,
|
||||
carry: c,
|
||||
time_to_expiry: t,
|
||||
kind,
|
||||
},
|
||||
volatility: vol,
|
||||
},
|
||||
);
|
||||
delta.push(g.delta);
|
||||
gamma.push(g.gamma);
|
||||
vega.push(g.vega);
|
||||
theta.push(g.theta);
|
||||
rho.push(g.rho);
|
||||
}
|
||||
|
||||
Ok((
|
||||
delta.into_pyarray(py),
|
||||
gamma.into_pyarray(py),
|
||||
vega.into_pyarray(py),
|
||||
theta.into_pyarray(py),
|
||||
rho.into_pyarray(py),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (price, underlying, strike, rate, time_to_expiry, option_type = "call", model = "bsm", carry = 0.0, initial_guess = 0.2, tolerance = 1e-8, max_iterations = 100))]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn implied_volatility(
|
||||
price: f64,
|
||||
underlying: f64,
|
||||
strike: f64,
|
||||
rate: f64,
|
||||
time_to_expiry: f64,
|
||||
option_type: &str,
|
||||
model: &str,
|
||||
carry: f64,
|
||||
initial_guess: f64,
|
||||
tolerance: f64,
|
||||
max_iterations: usize,
|
||||
) -> PyResult<f64> {
|
||||
let kind = super::parse_option_kind(option_type)?;
|
||||
let model = super::parse_pricing_model(model)?;
|
||||
Ok(ferro_ta_core::options::iv::implied_volatility(
|
||||
ferro_ta_core::options::OptionContract {
|
||||
model,
|
||||
underlying,
|
||||
strike,
|
||||
rate,
|
||||
carry,
|
||||
time_to_expiry,
|
||||
kind,
|
||||
},
|
||||
price,
|
||||
ferro_ta_core::options::IvSolverConfig {
|
||||
initial_guess,
|
||||
tolerance,
|
||||
max_iterations,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (price, underlying, strike, rate, time_to_expiry, option_type = "call", model = "bsm", carry = None, initial_guess = None, tolerance = 1e-8, max_iterations = 100))]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn implied_volatility_batch<'py>(
|
||||
py: Python<'py>,
|
||||
price: PyReadonlyArray1<'py, f64>,
|
||||
underlying: PyReadonlyArray1<'py, f64>,
|
||||
strike: PyReadonlyArray1<'py, f64>,
|
||||
rate: PyReadonlyArray1<'py, f64>,
|
||||
time_to_expiry: PyReadonlyArray1<'py, f64>,
|
||||
option_type: &str,
|
||||
model: &str,
|
||||
carry: Option<PyReadonlyArray1<'py, f64>>,
|
||||
initial_guess: Option<PyReadonlyArray1<'py, f64>>,
|
||||
tolerance: f64,
|
||||
max_iterations: usize,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let kind = super::parse_option_kind(option_type)?;
|
||||
let model = super::parse_pricing_model(model)?;
|
||||
let price = price.as_slice()?;
|
||||
let underlying = underlying.as_slice()?;
|
||||
let strike = strike.as_slice()?;
|
||||
let rate = rate.as_slice()?;
|
||||
let time_to_expiry = time_to_expiry.as_slice()?;
|
||||
let carry_vec = match carry {
|
||||
Some(array) => array.as_slice()?.to_vec(),
|
||||
None => vec![0.0; price.len()],
|
||||
};
|
||||
let guess_vec = match initial_guess {
|
||||
Some(array) => array.as_slice()?.to_vec(),
|
||||
None => vec![0.2; price.len()],
|
||||
};
|
||||
validation::validate_equal_length(&[
|
||||
(price.len(), "price"),
|
||||
(underlying.len(), "underlying"),
|
||||
(strike.len(), "strike"),
|
||||
(rate.len(), "rate"),
|
||||
(time_to_expiry.len(), "time_to_expiry"),
|
||||
(carry_vec.len(), "carry"),
|
||||
(guess_vec.len(), "initial_guess"),
|
||||
])?;
|
||||
|
||||
let out: Vec<f64> = price
|
||||
.iter()
|
||||
.zip(underlying.iter())
|
||||
.zip(strike.iter())
|
||||
.zip(rate.iter())
|
||||
.zip(time_to_expiry.iter())
|
||||
.zip(carry_vec.iter())
|
||||
.zip(guess_vec.iter())
|
||||
.map(|((((((&p, &u), &k), &r), &t), &c), &guess)| {
|
||||
ferro_ta_core::options::iv::implied_volatility(
|
||||
ferro_ta_core::options::OptionContract {
|
||||
model,
|
||||
underlying: u,
|
||||
strike: k,
|
||||
rate: r,
|
||||
carry: c,
|
||||
time_to_expiry: t,
|
||||
kind,
|
||||
},
|
||||
p,
|
||||
ferro_ta_core::options::IvSolverConfig {
|
||||
initial_guess: guess,
|
||||
tolerance,
|
||||
max_iterations,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (iv_series, window = 252))]
|
||||
pub fn iv_rank<'py>(
|
||||
py: Python<'py>,
|
||||
iv_series: PyReadonlyArray1<'py, f64>,
|
||||
window: i64,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let window = validation::parse_timeperiod(window, "window", 1)?;
|
||||
let out = ferro_ta_core::options::iv::iv_rank(iv_series.as_slice()?, window);
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (iv_series, window = 252))]
|
||||
pub fn iv_percentile<'py>(
|
||||
py: Python<'py>,
|
||||
iv_series: PyReadonlyArray1<'py, f64>,
|
||||
window: i64,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let window = validation::parse_timeperiod(window, "window", 1)?;
|
||||
let out = ferro_ta_core::options::iv::iv_percentile(iv_series.as_slice()?, window);
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (iv_series, window = 252))]
|
||||
pub fn iv_zscore<'py>(
|
||||
py: Python<'py>,
|
||||
iv_series: PyReadonlyArray1<'py, f64>,
|
||||
window: i64,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let window = validation::parse_timeperiod(window, "window", 1)?;
|
||||
let out = ferro_ta_core::options::iv::iv_zscore(iv_series.as_slice()?, window);
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//! PyO3 wrappers for options analytics.
|
||||
|
||||
mod chain;
|
||||
mod greeks;
|
||||
mod iv;
|
||||
mod pricing;
|
||||
mod surface;
|
||||
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
pub(crate) fn parse_option_kind(option_type: &str) -> PyResult<ferro_ta_core::options::OptionKind> {
|
||||
match option_type.to_ascii_lowercase().as_str() {
|
||||
"call" | "c" => Ok(ferro_ta_core::options::OptionKind::Call),
|
||||
"put" | "p" => Ok(ferro_ta_core::options::OptionKind::Put),
|
||||
_ => Err(PyValueError::new_err(format!(
|
||||
"option_type must be 'call' or 'put', got {option_type}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_pricing_model(model: &str) -> PyResult<ferro_ta_core::options::PricingModel> {
|
||||
match model.to_ascii_lowercase().as_str() {
|
||||
"bsm" | "black_scholes" | "black-scholes" | "blackscholes" => {
|
||||
Ok(ferro_ta_core::options::PricingModel::BlackScholes)
|
||||
}
|
||||
"black76" | "black_76" | "black-76" => Ok(ferro_ta_core::options::PricingModel::Black76),
|
||||
_ => Err(PyValueError::new_err(format!(
|
||||
"model must be one of 'bsm'/'black_scholes' or 'black76', got {model}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::pricing::bsm_price, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::pricing::black76_price, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::pricing::bsm_price_batch, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(
|
||||
self::pricing::black76_price_batch,
|
||||
m
|
||||
)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::greeks::option_greeks, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(
|
||||
self::greeks::option_greeks_batch,
|
||||
m
|
||||
)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::iv::implied_volatility, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(
|
||||
self::iv::implied_volatility_batch,
|
||||
m
|
||||
)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::iv::iv_rank, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::iv::iv_percentile, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::iv::iv_zscore, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::surface::smile_metrics, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(
|
||||
self::surface::term_structure_slope,
|
||||
m
|
||||
)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::chain::moneyness_labels, m)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(
|
||||
self::chain::select_strike_offset,
|
||||
m
|
||||
)?)?;
|
||||
m.add_function(pyo3::wrap_pyfunction!(self::chain::select_strike_delta, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
use crate::validation;
|
||||
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (spot, strike, rate, time_to_expiry, volatility, option_type = "call", dividend_yield = 0.0))]
|
||||
pub fn bsm_price(
|
||||
spot: f64,
|
||||
strike: f64,
|
||||
rate: f64,
|
||||
time_to_expiry: f64,
|
||||
volatility: f64,
|
||||
option_type: &str,
|
||||
dividend_yield: f64,
|
||||
) -> PyResult<f64> {
|
||||
let kind = super::parse_option_kind(option_type)?;
|
||||
Ok(ferro_ta_core::options::pricing::black_scholes_price(
|
||||
spot,
|
||||
strike,
|
||||
rate,
|
||||
dividend_yield,
|
||||
time_to_expiry,
|
||||
volatility,
|
||||
kind,
|
||||
))
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (forward, strike, rate, time_to_expiry, volatility, option_type = "call"))]
|
||||
pub fn black76_price(
|
||||
forward: f64,
|
||||
strike: f64,
|
||||
rate: f64,
|
||||
time_to_expiry: f64,
|
||||
volatility: f64,
|
||||
option_type: &str,
|
||||
) -> PyResult<f64> {
|
||||
let kind = super::parse_option_kind(option_type)?;
|
||||
Ok(ferro_ta_core::options::pricing::black_76_price(
|
||||
forward,
|
||||
strike,
|
||||
rate,
|
||||
time_to_expiry,
|
||||
volatility,
|
||||
kind,
|
||||
))
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (spot, strike, rate, time_to_expiry, volatility, dividend_yield, option_type = "call"))]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn bsm_price_batch<'py>(
|
||||
py: Python<'py>,
|
||||
spot: PyReadonlyArray1<'py, f64>,
|
||||
strike: PyReadonlyArray1<'py, f64>,
|
||||
rate: PyReadonlyArray1<'py, f64>,
|
||||
time_to_expiry: PyReadonlyArray1<'py, f64>,
|
||||
volatility: PyReadonlyArray1<'py, f64>,
|
||||
dividend_yield: PyReadonlyArray1<'py, f64>,
|
||||
option_type: &str,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let kind = super::parse_option_kind(option_type)?;
|
||||
let spot = spot.as_slice()?;
|
||||
let strike = strike.as_slice()?;
|
||||
let rate = rate.as_slice()?;
|
||||
let time_to_expiry = time_to_expiry.as_slice()?;
|
||||
let volatility = volatility.as_slice()?;
|
||||
let dividend_yield = dividend_yield.as_slice()?;
|
||||
validation::validate_equal_length(&[
|
||||
(spot.len(), "spot"),
|
||||
(strike.len(), "strike"),
|
||||
(rate.len(), "rate"),
|
||||
(time_to_expiry.len(), "time_to_expiry"),
|
||||
(volatility.len(), "volatility"),
|
||||
(dividend_yield.len(), "dividend_yield"),
|
||||
])?;
|
||||
|
||||
let out: Vec<f64> = spot
|
||||
.iter()
|
||||
.zip(strike.iter())
|
||||
.zip(rate.iter())
|
||||
.zip(time_to_expiry.iter())
|
||||
.zip(volatility.iter())
|
||||
.zip(dividend_yield.iter())
|
||||
.map(|(((((&s, &k), &r), &t), &vol), &q)| {
|
||||
ferro_ta_core::options::pricing::black_scholes_price(s, k, r, q, t, vol, kind)
|
||||
})
|
||||
.collect();
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (forward, strike, rate, time_to_expiry, volatility, option_type = "call"))]
|
||||
pub fn black76_price_batch<'py>(
|
||||
py: Python<'py>,
|
||||
forward: PyReadonlyArray1<'py, f64>,
|
||||
strike: PyReadonlyArray1<'py, f64>,
|
||||
rate: PyReadonlyArray1<'py, f64>,
|
||||
time_to_expiry: PyReadonlyArray1<'py, f64>,
|
||||
volatility: PyReadonlyArray1<'py, f64>,
|
||||
option_type: &str,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let kind = super::parse_option_kind(option_type)?;
|
||||
let forward = forward.as_slice()?;
|
||||
let strike = strike.as_slice()?;
|
||||
let rate = rate.as_slice()?;
|
||||
let time_to_expiry = time_to_expiry.as_slice()?;
|
||||
let volatility = volatility.as_slice()?;
|
||||
validation::validate_equal_length(&[
|
||||
(forward.len(), "forward"),
|
||||
(strike.len(), "strike"),
|
||||
(rate.len(), "rate"),
|
||||
(time_to_expiry.len(), "time_to_expiry"),
|
||||
(volatility.len(), "volatility"),
|
||||
])?;
|
||||
|
||||
let out: Vec<f64> = forward
|
||||
.iter()
|
||||
.zip(strike.iter())
|
||||
.zip(rate.iter())
|
||||
.zip(time_to_expiry.iter())
|
||||
.zip(volatility.iter())
|
||||
.map(|((((&f, &k), &r), &t), &vol)| {
|
||||
ferro_ta_core::options::pricing::black_76_price(f, k, r, t, vol, kind)
|
||||
})
|
||||
.collect();
|
||||
Ok(out.into_pyarray(py))
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use crate::validation;
|
||||
use numpy::PyReadonlyArray1;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (strikes, vols, reference_price, time_to_expiry, model = "bsm", rate = 0.0, carry = 0.0))]
|
||||
pub fn smile_metrics<'py>(
|
||||
strikes: PyReadonlyArray1<'py, f64>,
|
||||
vols: PyReadonlyArray1<'py, f64>,
|
||||
reference_price: f64,
|
||||
time_to_expiry: f64,
|
||||
model: &str,
|
||||
rate: f64,
|
||||
carry: f64,
|
||||
) -> PyResult<(f64, f64, f64, f64, f64)> {
|
||||
let strikes = strikes.as_slice()?;
|
||||
let vols = vols.as_slice()?;
|
||||
validation::validate_equal_length(&[(strikes.len(), "strikes"), (vols.len(), "vols")])?;
|
||||
let model = super::parse_pricing_model(model)?;
|
||||
let metrics = ferro_ta_core::options::surface::smile_metrics(
|
||||
strikes,
|
||||
vols,
|
||||
reference_price,
|
||||
rate,
|
||||
carry,
|
||||
time_to_expiry,
|
||||
model,
|
||||
);
|
||||
Ok((
|
||||
metrics.atm_iv,
|
||||
metrics.risk_reversal_25d,
|
||||
metrics.butterfly_25d,
|
||||
metrics.skew_slope,
|
||||
metrics.convexity,
|
||||
))
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
pub fn term_structure_slope<'py>(
|
||||
tenors: PyReadonlyArray1<'py, f64>,
|
||||
atm_ivs: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<f64> {
|
||||
let tenors = tenors.as_slice()?;
|
||||
let atm_ivs = atm_ivs.as_slice()?;
|
||||
validation::validate_equal_length(&[(tenors.len(), "tenors"), (atm_ivs.len(), "atm_ivs")])?;
|
||||
Ok(ferro_ta_core::options::surface::term_structure_slope(
|
||||
tenors, atm_ivs,
|
||||
))
|
||||
}
|
||||
Reference in New Issue
Block a user