chore: update ferro-ta version to 1.1.3 (#8)

- Bumped version numbers across Cargo.toml, Cargo.lock, pyproject.toml, and conda/meta.yaml to 1.1.3.
- Added new features including American option pricing, digital options, extended Greeks, and historical volatility estimators.
- Enhanced documentation and tests for new functionalities.
- Updated CHANGELOG.md to reflect changes for version 1.1.3.
This commit is contained in:
Pratik Bhadane
2026-04-02 16:38:32 +05:30
committed by GitHub
parent 125eb32d9f
commit 3e0f289d51
38 changed files with 5619 additions and 85 deletions
+127
View File
@@ -0,0 +1,127 @@
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
use ferro_ta_core::options::american::{
american_price_baw as core_american_price,
early_exercise_premium as core_early_exercise_premium,
};
#[pyfunction]
#[pyo3(signature = (underlying, strike, rate, time_to_expiry, volatility, option_type = "call", carry = 0.0))]
#[allow(clippy::too_many_arguments)]
pub fn american_price(
underlying: f64,
strike: f64,
rate: f64,
time_to_expiry: f64,
volatility: f64,
option_type: &str,
carry: f64,
) -> PyResult<f64> {
let kind = super::parse_option_kind(option_type)?;
Ok(core_american_price(
underlying,
strike,
rate,
carry,
time_to_expiry,
volatility,
kind,
))
}
#[pyfunction]
#[pyo3(signature = (underlying, strike, rate, time_to_expiry, volatility, option_type = "call", carry = None))]
#[allow(clippy::too_many_arguments)]
pub fn american_price_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,
carry: Option<PyReadonlyArray1<'py, f64>>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let kind = super::parse_option_kind(option_type)?;
let underlying = underlying.as_slice()?;
let strike = strike.as_slice()?;
let rate = rate.as_slice()?;
let tte = time_to_expiry.as_slice()?;
let vol = volatility.as_slice()?;
let n = underlying.len();
let carry_vec = match carry {
Some(arr) => arr.as_slice()?.to_vec(),
None => vec![0.0; n],
};
let out: Vec<f64> = underlying
.iter()
.zip(strike.iter())
.zip(rate.iter())
.zip(tte.iter())
.zip(vol.iter())
.zip(carry_vec.iter())
.map(|(((((&u, &k), &r), &t), &v), &c)| core_american_price(u, k, r, c, t, v, kind))
.collect();
Ok(out.into_pyarray(py))
}
#[pyfunction]
#[pyo3(signature = (underlying, strike, rate, time_to_expiry, volatility, option_type = "call", carry = 0.0))]
#[allow(clippy::too_many_arguments)]
pub fn early_exercise_premium(
underlying: f64,
strike: f64,
rate: f64,
time_to_expiry: f64,
volatility: f64,
option_type: &str,
carry: f64,
) -> PyResult<f64> {
let kind = super::parse_option_kind(option_type)?;
Ok(core_early_exercise_premium(
underlying,
strike,
rate,
carry,
time_to_expiry,
volatility,
kind,
))
}
#[pyfunction]
#[pyo3(signature = (underlying, strike, rate, time_to_expiry, volatility, option_type = "call", carry = None))]
#[allow(clippy::too_many_arguments)]
pub fn early_exercise_premium_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,
carry: Option<PyReadonlyArray1<'py, f64>>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let kind = super::parse_option_kind(option_type)?;
let underlying = underlying.as_slice()?;
let strike = strike.as_slice()?;
let rate = rate.as_slice()?;
let tte = time_to_expiry.as_slice()?;
let vol = volatility.as_slice()?;
let n = underlying.len();
let carry_vec = match carry {
Some(arr) => arr.as_slice()?.to_vec(),
None => vec![0.0; n],
};
let out: Vec<f64> = underlying
.iter()
.zip(strike.iter())
.zip(rate.iter())
.zip(tte.iter())
.zip(vol.iter())
.zip(carry_vec.iter())
.map(|(((((&u, &k), &r), &t), &v), &c)| core_early_exercise_premium(u, k, r, c, t, v, kind))
.collect();
Ok(out.into_pyarray(py))
}
+164
View File
@@ -0,0 +1,164 @@
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use ferro_ta_core::options::digital::{
digital_greeks as core_digital_greeks, digital_price as core_digital_price, DigitalKind,
};
fn parse_digital_kind(s: &str) -> PyResult<DigitalKind> {
match s.to_ascii_lowercase().replace('-', "_").as_str() {
"cash_or_nothing" | "cash" => Ok(DigitalKind::CashOrNothing),
"asset_or_nothing" | "asset" => Ok(DigitalKind::AssetOrNothing),
_ => Err(PyValueError::new_err(
"digital_type must be 'cash_or_nothing' or 'asset_or_nothing'",
)),
}
}
#[pyfunction]
#[pyo3(signature = (underlying, strike, rate, time_to_expiry, volatility, option_type = "call", digital_type = "cash_or_nothing", carry = 0.0))]
#[allow(clippy::too_many_arguments)]
pub fn digital_price(
underlying: f64,
strike: f64,
rate: f64,
time_to_expiry: f64,
volatility: f64,
option_type: &str,
digital_type: &str,
carry: f64,
) -> PyResult<f64> {
let kind = super::parse_option_kind(option_type)?;
let dkind = parse_digital_kind(digital_type)?;
Ok(core_digital_price(
underlying,
strike,
rate,
carry,
time_to_expiry,
volatility,
kind,
dkind,
))
}
#[pyfunction]
#[pyo3(signature = (underlying, strike, rate, time_to_expiry, volatility, option_type = "call", digital_type = "cash_or_nothing", carry = None))]
#[allow(clippy::too_many_arguments)]
pub fn digital_price_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,
digital_type: &str,
carry: Option<PyReadonlyArray1<'py, f64>>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let kind = super::parse_option_kind(option_type)?;
let dkind = parse_digital_kind(digital_type)?;
let underlying = underlying.as_slice()?;
let strike = strike.as_slice()?;
let rate = rate.as_slice()?;
let tte = time_to_expiry.as_slice()?;
let vol = volatility.as_slice()?;
let n = underlying.len();
let carry_vec = match carry {
Some(arr) => arr.as_slice()?.to_vec(),
None => vec![0.0; n],
};
let out: Vec<f64> = underlying
.iter()
.zip(strike.iter())
.zip(rate.iter())
.zip(tte.iter())
.zip(vol.iter())
.zip(carry_vec.iter())
.map(|(((((&u, &k), &r), &t), &v), &c)| core_digital_price(u, k, r, c, t, v, kind, dkind))
.collect();
Ok(out.into_pyarray(py))
}
#[pyfunction]
#[pyo3(signature = (underlying, strike, rate, time_to_expiry, volatility, option_type = "call", digital_type = "cash_or_nothing", carry = 0.0))]
#[allow(clippy::too_many_arguments)]
pub fn digital_greeks(
underlying: f64,
strike: f64,
rate: f64,
time_to_expiry: f64,
volatility: f64,
option_type: &str,
digital_type: &str,
carry: f64,
) -> PyResult<(f64, f64, f64)> {
let kind = super::parse_option_kind(option_type)?;
let dkind = parse_digital_kind(digital_type)?;
Ok(core_digital_greeks(
underlying,
strike,
rate,
carry,
time_to_expiry,
volatility,
kind,
dkind,
))
}
type GreekTriple<'py> = (
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", digital_type = "cash_or_nothing", carry = None))]
#[allow(clippy::too_many_arguments)]
pub fn digital_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,
digital_type: &str,
carry: Option<PyReadonlyArray1<'py, f64>>,
) -> PyResult<GreekTriple<'py>> {
let kind = super::parse_option_kind(option_type)?;
let dkind = parse_digital_kind(digital_type)?;
let underlying = underlying.as_slice()?;
let strike = strike.as_slice()?;
let rate = rate.as_slice()?;
let tte = time_to_expiry.as_slice()?;
let vol = volatility.as_slice()?;
let n = underlying.len();
let carry_vec = match carry {
Some(arr) => arr.as_slice()?.to_vec(),
None => vec![0.0; n],
};
let mut delta = Vec::with_capacity(n);
let mut gamma = Vec::with_capacity(n);
let mut vega = Vec::with_capacity(n);
for (((((&u, &k), &r), &t), &v), &c) in underlying
.iter()
.zip(strike.iter())
.zip(rate.iter())
.zip(tte.iter())
.zip(vol.iter())
.zip(carry_vec.iter())
{
let (d, g, ve) = core_digital_greeks(u, k, r, c, t, v, kind, dkind);
delta.push(d);
gamma.push(g);
vega.push(ve);
}
Ok((
delta.into_pyarray(py),
gamma.into_pyarray(py),
vega.into_pyarray(py),
))
}
+117
View File
@@ -2,6 +2,14 @@ use crate::validation;
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
type ExtendedGreekArrays<'py> = (
Bound<'py, PyArray1<f64>>,
Bound<'py, PyArray1<f64>>,
Bound<'py, PyArray1<f64>>,
Bound<'py, PyArray1<f64>>,
Bound<'py, PyArray1<f64>>,
);
type GreekArrays<'py> = (
Bound<'py, PyArray1<f64>>,
Bound<'py, PyArray1<f64>>,
@@ -123,3 +131,112 @@ pub fn option_greeks_batch<'py>(
rho.into_pyarray(py),
))
}
#[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 extended_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 eg = ferro_ta_core::options::greeks::model_extended_greeks(
ferro_ta_core::options::OptionEvaluation {
contract: ferro_ta_core::options::OptionContract {
model,
underlying,
strike,
rate,
carry,
time_to_expiry,
kind,
},
volatility,
},
);
Ok((eg.vanna, eg.volga, eg.charm, eg.speed, eg.color))
}
#[pyfunction]
#[pyo3(signature = (underlying, strike, rate, time_to_expiry, volatility, option_type = "call", model = "bsm", carry = None))]
#[allow(clippy::too_many_arguments)]
pub fn extended_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<ExtendedGreekArrays<'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 vanna = Vec::with_capacity(underlying.len());
let mut volga = Vec::with_capacity(underlying.len());
let mut charm = Vec::with_capacity(underlying.len());
let mut speed = Vec::with_capacity(underlying.len());
let mut color = 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 eg = ferro_ta_core::options::greeks::model_extended_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,
},
);
vanna.push(eg.vanna);
volga.push(eg.volga);
charm.push(eg.charm);
speed.push(eg.speed);
color.push(eg.color);
}
Ok((
vanna.into_pyarray(py),
volga.into_pyarray(py),
charm.into_pyarray(py),
speed.into_pyarray(py),
color.into_pyarray(py),
))
}
+64
View File
@@ -1,10 +1,13 @@
//! PyO3 wrappers for options analytics.
mod american;
mod chain;
mod digital;
mod greeks;
mod iv;
mod payoff;
mod pricing;
mod realized_vol;
mod surface;
use pyo3::exceptions::PyValueError;
@@ -40,11 +43,20 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
self::pricing::black76_price_batch,
m
)?)?;
m.add_function(pyo3::wrap_pyfunction!(
self::pricing::put_call_parity_deviation,
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::greeks::extended_greeks, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(
self::greeks::extended_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,
@@ -58,6 +70,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
self::surface::term_structure_slope,
m
)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::surface::expected_move, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::chain::moneyness_labels, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(
self::chain::select_strike_offset,
@@ -80,5 +93,56 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
self::payoff::aggregate_greeks_legs,
m
)?)?;
m.add_function(pyo3::wrap_pyfunction!(
self::payoff::strategy_value_dense,
m
)?)?;
// Digital options
m.add_function(pyo3::wrap_pyfunction!(self::digital::digital_price, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(
self::digital::digital_price_batch,
m
)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::digital::digital_greeks, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(
self::digital::digital_greeks_batch,
m
)?)?;
// American options
m.add_function(pyo3::wrap_pyfunction!(self::american::american_price, m)?)?;
m.add_function(pyo3::wrap_pyfunction!(
self::american::american_price_batch,
m
)?)?;
m.add_function(pyo3::wrap_pyfunction!(
self::american::early_exercise_premium,
m
)?)?;
m.add_function(pyo3::wrap_pyfunction!(
self::american::early_exercise_premium_batch,
m
)?)?;
// Historical volatility estimators + vol cone
m.add_function(pyo3::wrap_pyfunction!(
self::realized_vol::close_to_close_vol,
m
)?)?;
m.add_function(pyo3::wrap_pyfunction!(
self::realized_vol::parkinson_vol,
m
)?)?;
m.add_function(pyo3::wrap_pyfunction!(
self::realized_vol::garman_klass_vol,
m
)?)?;
m.add_function(pyo3::wrap_pyfunction!(
self::realized_vol::rogers_satchell_vol,
m
)?)?;
m.add_function(pyo3::wrap_pyfunction!(
self::realized_vol::yang_zhang_vol,
m
)?)?;
m.add_function(pyo3::wrap_pyfunction!(self::realized_vol::vol_cone, m)?)?;
Ok(())
}
+59 -7
View File
@@ -7,6 +7,7 @@ use pyo3::types::{PyAny, PyTuple};
enum Instrument {
Option,
Future,
Stock,
}
#[derive(Clone, Copy)]
@@ -34,8 +35,9 @@ fn parse_instrument(v: i64) -> PyResult<Instrument> {
match v {
0 => Ok(Instrument::Option),
1 => Ok(Instrument::Future),
2 => Ok(Instrument::Stock),
_ => Err(PyValueError::new_err(
"instrument must be 0 (option) or 1 (future)",
"instrument must be 0 (option), 1 (future), or 2 (stock)",
)),
}
}
@@ -62,8 +64,9 @@ fn parse_instrument_label(v: &str) -> PyResult<Instrument> {
match v.to_ascii_lowercase().as_str() {
"option" => Ok(Instrument::Option),
"future" => Ok(Instrument::Future),
"stock" => Ok(Instrument::Stock),
_ => Err(PyValueError::new_err(
"instrument must be 'option' or 'future'",
"instrument must be 'option', 'future', or 'stock'",
)),
}
}
@@ -202,7 +205,7 @@ pub fn strategy_payoff_dense<'py>(
total[i] += leg_scale * (intrinsic - p);
}
}
Instrument::Future => {
Instrument::Future | Instrument::Stock => {
let e = entry[leg_idx];
for (i, &s) in grid.iter().enumerate() {
total[i] += leg_scale * (s - e);
@@ -253,9 +256,9 @@ pub fn strategy_payoff_legs<'py>(
total[i] += leg_scale * (intrinsic - premium);
}
}
Instrument::Future => {
Instrument::Future | Instrument::Stock => {
let entry_price = leg_attr_optional_f64(&leg, "entry_price")?.ok_or_else(|| {
PyValueError::new_err("Futures payoff legs require entry_price.")
PyValueError::new_err("Futures/stock payoff legs require entry_price.")
})?;
for (i, &s) in grid.iter().enumerate() {
total[i] += leg_scale * (s - entry_price);
@@ -323,7 +326,7 @@ pub fn aggregate_greeks_dense(
let side_sign = parse_side(side[i])?.sign();
let leg_scale = side_sign * qty[i] * mult[i];
match instrument {
Instrument::Future => {
Instrument::Future | Instrument::Stock => {
delta += leg_scale;
}
Instrument::Option => {
@@ -382,7 +385,7 @@ pub fn aggregate_greeks_legs(
let leg_scale = side_sign * quantity * multiplier;
match instrument {
Instrument::Future => {
Instrument::Future | Instrument::Stock => {
delta += leg_scale;
}
Instrument::Option => {
@@ -441,3 +444,52 @@ pub fn aggregate_greeks_legs(
Ok((delta, gamma, vega, theta, rho))
}
/// Compute BSM-based strategy value over a spot grid (pre-expiry mark-to-market).
///
/// Unlike `strategy_payoff_dense` (which uses intrinsic at expiry), this function
/// values each option leg using the Black-Scholes model price. Futures and stock
/// legs are valued the same as in `strategy_payoff_dense`.
///
/// Delegates to `ferro_ta_core::options::payoff::strategy_value_grid`.
///
/// NOTE: `crates/ferro_ta_core/src/options/mod.rs` must declare `pub mod payoff;`
/// for this function to compile.
#[pyfunction]
#[allow(clippy::too_many_arguments)]
pub fn strategy_value_dense<'py>(
py: Python<'py>,
spot_grid: PyReadonlyArray1<'py, f64>,
instruments: PyReadonlyArray1<'py, i64>,
sides: PyReadonlyArray1<'py, i64>,
option_types: PyReadonlyArray1<'py, i64>,
strikes: PyReadonlyArray1<'py, f64>,
premiums: PyReadonlyArray1<'py, f64>,
entry_prices: PyReadonlyArray1<'py, f64>,
quantities: PyReadonlyArray1<'py, f64>,
multipliers: PyReadonlyArray1<'py, f64>,
time_to_expiries: PyReadonlyArray1<'py, f64>,
volatilities: PyReadonlyArray1<'py, f64>,
rates_per_leg: PyReadonlyArray1<'py, f64>,
carries_per_leg: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let grid = spot_grid.as_slice()?;
let inst = instruments.as_slice()?;
let side = sides.as_slice()?;
let opt_t = option_types.as_slice()?;
let strike = strikes.as_slice()?;
let premium = premiums.as_slice()?;
let entry = entry_prices.as_slice()?;
let qty = quantities.as_slice()?;
let mult = multipliers.as_slice()?;
let tte = time_to_expiries.as_slice()?;
let vol = volatilities.as_slice()?;
let rate = rates_per_leg.as_slice()?;
let carry = carries_per_leg.as_slice()?;
let result = ferro_ta_core::options::payoff::strategy_value_grid(
grid, inst, side, opt_t, strike, premium, entry, qty, mult, tte, vol, rate, carry,
);
Ok(result.into_pyarray(py))
}
+23
View File
@@ -89,6 +89,29 @@ pub fn bsm_price_batch<'py>(
Ok(out.into_pyarray(py))
}
#[pyfunction]
#[pyo3(signature = (call_price, put_price, spot, strike, rate, time_to_expiry, carry = 0.0))]
#[allow(clippy::too_many_arguments)]
pub fn put_call_parity_deviation(
call_price: f64,
put_price: f64,
spot: f64,
strike: f64,
rate: f64,
time_to_expiry: f64,
carry: f64,
) -> PyResult<f64> {
Ok(ferro_ta_core::options::pricing::put_call_parity_deviation(
call_price,
put_price,
spot,
strike,
rate,
carry,
time_to_expiry,
))
}
#[pyfunction]
#[pyo3(signature = (forward, strike, rate, time_to_expiry, volatility, option_type = "call"))]
pub fn black76_price_batch<'py>(
+115
View File
@@ -0,0 +1,115 @@
use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;
use ferro_ta_core::options::realized_vol as core;
#[pyfunction]
#[pyo3(signature = (close, window, trading_days = 252.0))]
pub fn close_to_close_vol<'py>(
py: Python<'py>,
close: PyReadonlyArray1<'py, f64>,
window: usize,
trading_days: f64,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
Ok(core::close_to_close_vol(close.as_slice()?, window, trading_days).into_pyarray(py))
}
#[pyfunction]
#[pyo3(signature = (high, low, window, trading_days = 252.0))]
pub fn parkinson_vol<'py>(
py: Python<'py>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
window: usize,
trading_days: f64,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
Ok(
core::parkinson_vol(high.as_slice()?, low.as_slice()?, window, trading_days)
.into_pyarray(py),
)
}
#[pyfunction]
#[pyo3(signature = (open, high, low, close, window, trading_days = 252.0))]
#[allow(clippy::too_many_arguments)]
pub fn garman_klass_vol<'py>(
py: Python<'py>,
open: PyReadonlyArray1<'py, f64>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
window: usize,
trading_days: f64,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
Ok(core::garman_klass_vol(
open.as_slice()?,
high.as_slice()?,
low.as_slice()?,
close.as_slice()?,
window,
trading_days,
)
.into_pyarray(py))
}
#[pyfunction]
#[pyo3(signature = (open, high, low, close, window, trading_days = 252.0))]
#[allow(clippy::too_many_arguments)]
pub fn rogers_satchell_vol<'py>(
py: Python<'py>,
open: PyReadonlyArray1<'py, f64>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
window: usize,
trading_days: f64,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
Ok(core::rogers_satchell_vol(
open.as_slice()?,
high.as_slice()?,
low.as_slice()?,
close.as_slice()?,
window,
trading_days,
)
.into_pyarray(py))
}
#[pyfunction]
#[pyo3(signature = (open, high, low, close, window, trading_days = 252.0))]
#[allow(clippy::too_many_arguments)]
pub fn yang_zhang_vol<'py>(
py: Python<'py>,
open: PyReadonlyArray1<'py, f64>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
window: usize,
trading_days: f64,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
Ok(core::yang_zhang_vol(
open.as_slice()?,
high.as_slice()?,
low.as_slice()?,
close.as_slice()?,
window,
trading_days,
)
.into_pyarray(py))
}
/// Returns a list of (window, min, p25, median, p75, max) tuples.
#[allow(clippy::type_complexity)]
#[pyfunction]
#[pyo3(signature = (close, windows, trading_days = 252.0))]
pub fn vol_cone(
close: PyReadonlyArray1<'_, f64>,
windows: Vec<usize>,
trading_days: f64,
) -> PyResult<Vec<(usize, f64, f64, f64, f64, f64)>> {
let slices = core::vol_cone(close.as_slice()?, &windows, trading_days);
Ok(slices
.into_iter()
.map(|s| (s.window, s.min, s.p25, s.median, s.p75, s.max))
.collect())
}
+16
View File
@@ -47,3 +47,19 @@ pub fn term_structure_slope<'py>(
tenors, atm_ivs,
))
}
#[pyfunction]
#[pyo3(signature = (spot, iv, days_to_expiry, trading_days_per_year = 252.0))]
pub fn expected_move(
spot: f64,
iv: f64,
days_to_expiry: f64,
trading_days_per_year: f64,
) -> PyResult<(f64, f64)> {
Ok(ferro_ta_core::options::surface::expected_move(
spot,
iv,
days_to_expiry,
trading_days_per_year,
))
}