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:
@@ -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))
|
||||
}
|
||||
@@ -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),
|
||||
))
|
||||
}
|
||||
@@ -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),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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,
|
||||
))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user