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
+4 -4
View File
@@ -13,7 +13,7 @@ fn price_return(curr: f64, prev: f64) -> f64 {
fn beta_fallback(x: &[f64], y: &[f64], timeperiod: usize) -> Vec<f64> {
let n = x.len();
let mut result = vec![f64::NAN; n];
for end in timeperiod..n {
for (end, slot) in result.iter_mut().enumerate().take(n).skip(timeperiod) {
let start = end - timeperiod;
let mut rx = vec![0.0_f64; timeperiod];
let mut ry = vec![0.0_f64; timeperiod];
@@ -36,7 +36,7 @@ fn beta_fallback(x: &[f64], y: &[f64], timeperiod: usize) -> Vec<f64> {
.map(|&value| (value - mean_x).powi(2))
.sum::<f64>()
/ timeperiod as f64;
result[end] = if var_x != 0.0 { cov / var_x } else { f64::NAN };
*slot = if var_x != 0.0 { cov / var_x } else { f64::NAN };
}
result
}
@@ -102,8 +102,8 @@ pub fn beta<'py>(
}
}
for end in timeperiod..n {
result[end] = if invalid_pairs == 0 {
for (end, slot) in result.iter_mut().enumerate().take(n).skip(timeperiod) {
*slot = if invalid_pairs == 0 {
let denom = period * sum_rx2 - sum_rx * sum_rx;
if denom != 0.0 {
(period * sum_rxry - sum_rx * sum_ry) / denom
+4 -4
View File
@@ -5,7 +5,7 @@ use pyo3::prelude::*;
fn correl_fallback(x: &[f64], y: &[f64], timeperiod: usize) -> Vec<f64> {
let n = x.len();
let mut result = vec![f64::NAN; n];
for end in (timeperiod - 1)..n {
for (end, slot) in result.iter_mut().enumerate().take(n).skip(timeperiod - 1) {
let wx = &x[(end + 1 - timeperiod)..=end];
let wy = &y[(end + 1 - timeperiod)..=end];
let mean_x = wx.iter().sum::<f64>() / timeperiod as f64;
@@ -26,7 +26,7 @@ fn correl_fallback(x: &[f64], y: &[f64], timeperiod: usize) -> Vec<f64> {
.sum::<f64>()
.sqrt();
let denom = std_x * std_y;
result[end] = if denom != 0.0 { cov / denom } else { f64::NAN };
*slot = if denom != 0.0 { cov / denom } else { f64::NAN };
}
result
}
@@ -72,10 +72,10 @@ pub fn correl<'py>(
.map(|(&lhs, &rhs)| lhs * rhs)
.sum::<f64>();
for end in (timeperiod - 1)..n {
for (end, slot) in result.iter_mut().enumerate().take(n).skip(timeperiod - 1) {
let denom_x = period * sum_x2 - sum_x * sum_x;
let denom_y = period * sum_y2 - sum_y * sum_y;
result[end] = if denom_x > 0.0 && denom_y > 0.0 {
*slot = if denom_x > 0.0 && denom_y > 0.0 {
(period * sum_xy - sum_x * sum_y) / (denom_x * denom_y).sqrt()
} else {
f64::NAN