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:
Pratik Bhadane
2026-03-24 02:41:50 +05:30
co-authored by Claude Sonnet 4.6
parent 2d5000262f
commit 602d675749
47 changed files with 4538 additions and 280 deletions
+49
View File
@@ -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,
))
}