Analysis Modules

Parallel parameter sweeps

Grid-search a strategy over every parameter combination in one call. Signal generation runs once per combination in Python (it calls the Rust indicators); every backtest and metric then runs in Rust, in parallel, with the GIL released.

The result keeps the full returns matrix, so the same sweep feeds straight into the overfitting diagnostics in ferro_ta.analysis.overfit:

>>> import numpy as np
>>> from ferro_ta.analysis.sweep import sweep
>>> close = np.cumprod(1 + np.random.default_rng(1).normal(0, 0.01, 2000)) * 100
>>> res = sweep(close, "sma_crossover", {"fast": [5, 10, 20], "slow": [50, 100, 200]})
>>> res.best_params("sharpe")
{'fast': 10, 'slow': 100}
>>> res.deflated_sharpe()                  # selection-bias corrected
>>> res.pbo(n_splits=10).pbo               # probability of overfitting
class ferro_ta.analysis.sweep.SweepResult(params, returns, metrics)[source]

Bases: object

Output of sweep().

Parameters:
  • params (tuple[Mapping]) – Parameters of each combination (column order of returns).

  • returns (NDArray[np.float64], shape (n_bars, n_combos)) – Per-bar strategy returns of every combination.

  • metrics (dict[str, NDArray[np.float64]]) – Each performance metric as an array of length n_combos.

  • Immutable (arrays are read-only and mappings are read-only proxies.)

  • dict. (best_params() returns a fresh)

best_index(metric='sharpe', *, minimize=False, radius=0)[source]

Column index of the best combination by metric (NaN-aware).

radius=0 takes the single best cell. radius >= 1 scores each cell by the mean of its grid neighbourhood first, so a broad stable region beats an isolated spike, which is usually noise.

Parameters:
Return type:

int

best_params(metric='sharpe', *, minimize=False, radius=0)[source]

Parameter dict of the best combination (see best_index()).

Parameters:
Return type:

dict[str, Any]

deflated_sharpe(metric='sharpe', *, minimize=False, radius=0)[source]

Deflated Sharpe Ratio of the combination selected by metric.

Deflates against all n_trials combinations, so the more you searched the higher the bar. metric only picks the winner; the deflation itself always uses raw per-period Sharpe (no risk-free adjustment), as the DSR formula requires.

Parameters:
Return type:

float

metrics: Mapping[str, NDArray[float64]]
property n_trials: int
params: tuple[Mapping[str, Any], ...]
pbo(n_splits=16)[source]

Probability of Backtest Overfitting across all combinations.

Parameters:

n_splits (int)

Return type:

PBOResult

pivot(metric, row, col)[source]

Heatmap-ready (row_values, col_values, matrix) for two params.

When other parameters also vary, each cell holds the best (max) value over them; cells with no combination are NaN.

Parameters:
Return type:

tuple[list[Any], list[Any], NDArray[float64]]

returns: NDArray[float64]
to_frame()[source]

pandas.DataFrame with one row per combination (params + metrics).

Return type:

Any

ferro_ta.analysis.sweep.expand_grid(param_grid)[source]

Normalise a grid to a tuple of parameter dicts.

A mapping of name -> values expands to its Cartesian product (keys in insertion order); a sequence of dicts is used as-is.

Parameters:

param_grid (Mapping[str, Sequence[Any]] | Sequence[Mapping[str, Any]])

Return type:

tuple[dict[str, Any], …]

ferro_ta.analysis.sweep.sweep(close, strategy, param_grid, *, commission_per_trade=0.0, slippage_bps=0.0, initial_capital=100000.0, periods_per_year=252.0, risk_free_rate=0.0, parallel=True)[source]

Backtest strategy for every combination in param_grid.

Parameters:
  • close (array-like) – Close prices.

  • strategy (str or callable) – Strategy name from ferro_ta.analysis.backtest.backtest()’s registry, or fn(close, **params) -> signals. Names in RUST_BUILTIN_STRATEGIES (sma_crossover, rsi_30_70, macd_crossover) run entirely in Rust, signal generation included, and silently skip combinations that violate their constraints (e.g. fast >= slow), so SweepResult.params lists only the combinations that ran. Callables run once per combination in Python and any exception they raise aborts the sweep.

  • param_grid (mapping or sequence of mappings) – {"fast": [5, 10], "slow": [50, 100]} (Cartesian product) or an explicit list of parameter dicts.

  • commission_per_trade (float) – Costs, with the same semantics as backtest().

  • slippage_bps (float) – Costs, with the same semantics as backtest().

  • initial_capital (float) – Costs, with the same semantics as backtest().

  • periods_per_year (float) – Annualisation settings for the metrics.

  • risk_free_rate (float) – Annualisation settings for the metrics.

  • parallel (bool) – Run combinations across threads (default True).

Return type:

SweepResult

Backtest-overfitting statistics

Run enough parameter combinations and one of them will look brilliant by chance. This module quantifies how much of a backtest’s edge survives that selection bias. Every heavy computation runs in Rust.

  • probabilistic_sharpe_ratio() — P(true Sharpe > benchmark), corrected for sample length, skewness and fat tails (Bailey & López de Prado, 2012).

  • deflated_sharpe_ratio() — PSR measured against the best Sharpe you would expect from N zero-skill trials (Bailey & López de Prado, 2014).

  • pbo() — Probability of Backtest Overfitting via combinatorially symmetric cross-validation (Bailey, Borwein, López de Prado & Zhu, 2017).

  • purged_kfold() — k-fold splits with purge/embargo gaps for labels that overlap in time.

All Sharpe ratios in this module are per-period (mean / std of the raw returns, not annualised).

Example

>>> import numpy as np
>>> from ferro_ta.analysis.overfit import deflated_sharpe_ratio, pbo, per_period_sharpe
>>> rng = np.random.default_rng(0)
>>> trials = rng.normal(0.0, 0.01, size=(1000, 50))   # 50 noise strategies
>>> best = trials[:, trials.mean(0).argmax()]
>>> dsr = deflated_sharpe_ratio(best, trial_sharpes=per_period_sharpe(trials))
>>> result = pbo(trials, n_splits=10)                 # ~0.5 for pure noise
class ferro_ta.analysis.overfit.PBOResult(pbo, logits, is_sharpe, oos_sharpe, best_index)[source]

Bases: object

Result of pbo(); arrays have one entry per IS/OOS combination.

Immutable: arrays are read-only.

Parameters:
  • pbo (float) – Probability of Backtest Overfitting — fraction of combinations where the in-sample winner landed at or below the out-of-sample median.

  • logits (NDArray[np.float64]) – Logit of the IS winner’s OOS relative rank (> 0 is good).

  • is_sharpe (NDArray[np.float64]) – Per-period Sharpe of the IS winner, in and out of sample. Regressing oos_sharpe on is_sharpe gives the performance degradation.

  • oos_sharpe (NDArray[np.float64]) – Per-period Sharpe of the IS winner, in and out of sample. Regressing oos_sharpe on is_sharpe gives the performance degradation.

  • best_index (NDArray[np.int64]) – Column chosen as IS winner in each combination.

best_index: NDArray[int64]
is_sharpe: NDArray[float64]
logits: NDArray[float64]
property n_combinations: int
oos_sharpe: NDArray[float64]
pbo: float
property prob_oos_loss: float

Fraction of combinations where the IS winner lost money OOS.

ferro_ta.analysis.overfit.deflated_sharpe_ratio(returns, *, trial_sharpes=None, n_trials=None, trial_sharpe_variance=None)[source]

Deflated Sharpe Ratio of the selected strategy.

Pass either trial_sharpes (per-period Sharpe of every configuration tried, including the selected one) or both n_trials and trial_sharpe_variance.

A DSR above ~0.95 suggests the edge survives the multiple-testing correction; values near or below 0.5 mean the best result is consistent with luck.

Parameters:
  • returns (ArrayLike)

  • trial_sharpes (ArrayLike | None)

  • n_trials (int | None)

  • trial_sharpe_variance (float | None)

Return type:

float

ferro_ta.analysis.overfit.expected_max_sharpe(n_trials, trial_sharpe_variance)[source]

Expected best per-period Sharpe among n_trials zero-skill trials.

Parameters:
  • n_trials (int)

  • trial_sharpe_variance (float)

Return type:

float

ferro_ta.analysis.overfit.pbo(returns_2d, n_splits=16)[source]

Probability of Backtest Overfitting via CSCV.

Parameters:
  • returns_2d (array-like, shape (n_bars, n_strategies)) – Per-period returns, one column per parameter configuration — e.g. ferro_ta.analysis.sweep.SweepResult.returns.

  • n_splits (int) – Even number of contiguous time blocks (2–20). Evaluates C(n_splits, n_splits / 2) combinations: 12 870 for the default 16.

Return type:

PBOResult

ferro_ta.analysis.overfit.per_period_sharpe(returns_2d)[source]

Per-period Sharpe (mean / sample std) of each column; 0 where std is 0.

Parameters:

returns_2d (ArrayLike)

Return type:

NDArray[float64]

ferro_ta.analysis.overfit.probabilistic_sharpe_ratio(returns, benchmark_sharpe=0.0)[source]

Probability that the true per-period Sharpe exceeds benchmark_sharpe.

Parameters:
  • returns (array-like) – Per-period strategy returns (at least 3, finite, non-constant).

  • benchmark_sharpe (float) – Per-period Sharpe to beat (default 0).

Returns:

PSR in [0, 1]; NaN when the skew/kurtosis correction is undefined.

Return type:

float

ferro_ta.analysis.overfit.purged_kfold(n_bars, n_folds=5, *, purge_bars=0, embargo_bars=0)[source]

Purged/embargoed k-fold as (train_idx, test_idx) index arrays.

purge_bars should be at least your label horizon; embargo_bars guards against serial correlation leaking from test back into training.

Parameters:
  • n_bars (int)

  • n_folds (int)

  • purge_bars (int)

  • embargo_bars (int)

Return type:

list[tuple[NDArray[int64], NDArray[int64]]]

ferro_ta.analysis.overfit.purged_kfold_indices(n_bars, n_folds=5, *, purge_bars=0, embargo_bars=0)[source]

Fold boundaries, shape (n_folds, 4).

Columns are [train_left_end, test_start, test_end, train_right_start]: train = [0, train_left_end) ∪ [train_right_start, n_bars).

Parameters:
  • n_bars (int)

  • n_folds (int)

  • purge_bars (int)

  • embargo_bars (int)

Return type:

NDArray[int64]

ferro_ta.analysis.options — Rust-backed derivatives analytics for options.

This module preserves the legacy IV-series helpers and expands them with pricing, Greeks, implied-volatility inversion, smile analytics, and strike selection helpers suitable for research and simulation workflows.

class ferro_ta.analysis.options.ExtendedGreeks(vanna, volga, charm, speed, color)[source]

Bases: object

Container for second-order and cross Greeks.

Parameters:
  • vanna (float | NDArray[float64])

  • volga (float | NDArray[float64])

  • charm (float | NDArray[float64])

  • speed (float | NDArray[float64])

  • color (float | NDArray[float64])

charm: float | NDArray[float64]
color: float | NDArray[float64]
speed: float | NDArray[float64]
to_dict()[source]
Return type:

dict[str, float | NDArray[float64]]

vanna: float | NDArray[float64]
volga: float | NDArray[float64]
class ferro_ta.analysis.options.OptionGreeks(delta, gamma, vega, theta, rho)[source]

Bases: object

Container for first-order Greeks.

Parameters:
  • delta (float | NDArray[float64])

  • gamma (float | NDArray[float64])

  • vega (float | NDArray[float64])

  • theta (float | NDArray[float64])

  • rho (float | NDArray[float64])

delta: float | NDArray[float64]
gamma: float | NDArray[float64]
rho: float | NDArray[float64]
theta: float | NDArray[float64]
to_dict()[source]
Return type:

dict[str, float | NDArray[float64]]

vega: float | NDArray[float64]
class ferro_ta.analysis.options.SmileMetrics(atm_iv, risk_reversal_25d, butterfly_25d, skew_slope, convexity)[source]

Bases: object

Summary metrics for a single smile slice.

Parameters:
atm_iv: float
butterfly_25d: float
convexity: float
risk_reversal_25d: float
skew_slope: float
to_dict()[source]
Return type:

dict[str, float]

class ferro_ta.analysis.options.VolCone(windows, min, p25, median, p75, max)[source]

Bases: object

Historical realized vol distribution across window lengths.

Parameters:
  • windows (NDArray[float64])

  • min (NDArray[float64])

  • p25 (NDArray[float64])

  • median (NDArray[float64])

  • p75 (NDArray[float64])

  • max (NDArray[float64])

max: NDArray[float64]
median: NDArray[float64]
min: NDArray[float64]
p25: NDArray[float64]
p75: NDArray[float64]
to_dict()[source]
Return type:

dict[str, NDArray[float64]]

windows: NDArray[float64]
ferro_ta.analysis.options.american_option_price(underlying, strike, rate, time_to_expiry, volatility, *, option_type='call', carry=0.0)[source]

American option price using the Barone-Adesi-Whaley (1987) approximation.

Accurate to within a few basis points for standard equity/index parameters. O(1) per evaluation — suitable for batch pricing or calibration.

Parameters:
  • underlying (ArrayLike | float) – Current underlying (spot) price.

  • strike (ArrayLike | float) – Option strike price.

  • rate (ArrayLike | float) – Risk-free rate (annualised, decimal).

  • time_to_expiry (ArrayLike | float) – Time to expiry in years.

  • volatility (ArrayLike | float) – Implied volatility (annualised, decimal).

  • option_type (str) – "call" (default) or "put".

  • carry (ArrayLike | float) – Continuous carry / dividend yield (annualised, decimal). Default 0. For calls with carry = 0 (no dividends) early exercise is never optimal and the result equals the European BSM price.

Returns:

American option price ≥ European BSM price.

Return type:

float or NDArray[float64]

Notes

The BAW approximation uses a quadratic equation to find the critical exercise boundary S* via Newton-Raphson iteration, then adds the early exercise premium on top of the European price.

Reference: Barone-Adesi, G. & Whaley, R.E. (1987). “Efficient Analytic Approximation of American Option Values.” Journal of Finance, 42(2), 301–320.

See also

early_exercise_premium

Difference between American and European prices.

ferro_ta.analysis.options.black_76_price(forward, strike, rate, time_to_expiry, volatility, *, option_type='call')[source]

Price options under Black-76.

Parameters:
  • forward (ArrayLike | float)

  • strike (ArrayLike | float)

  • rate (ArrayLike | float)

  • time_to_expiry (ArrayLike | float)

  • volatility (ArrayLike | float)

  • option_type (str)

Return type:

float | NDArray[float64]

ferro_ta.analysis.options.black_scholes_price(spot, strike, rate, time_to_expiry, volatility, *, option_type='call', dividend_yield=0.0)[source]

Price options under Black-Scholes-Merton.

Parameters:
  • spot (ArrayLike | float)

  • strike (ArrayLike | float)

  • rate (ArrayLike | float)

  • time_to_expiry (ArrayLike | float)

  • volatility (ArrayLike | float)

  • option_type (str)

  • dividend_yield (ArrayLike | float)

Return type:

float | NDArray[float64]

ferro_ta.analysis.options.close_to_close_vol(close, window=20, trading_days_per_year=252.0)[source]

Rolling close-to-close realized volatility (annualised).

Baseline estimator — uses only closing prices. Less efficient than OHLC estimators but requires only daily close data.

Parameters:
  • close (ArrayLike) – Array of closing prices (length ≥ window + 1).

  • window (int) – Rolling look-back period in bars (default 20).

  • trading_days_per_year (float) – Annualisation factor (default 252).

Returns:

Same length as close. First window values are NaN.

Return type:

NDArray[float64]

Notes

Formula:

σ = √( Σᵢ ln²(Cᵢ/Cᵢ₋₁) / window × trading_days_per_year )

No Bessel correction is applied (population variance, not sample variance).

ferro_ta.analysis.options.digital_option_greeks(underlying, strike, rate, time_to_expiry, volatility, *, option_type='call', digital_type='cash_or_nothing', carry=0.0)[source]

Delta, gamma, and vega for a digital option via numerical bumping.

Uses central finite differences (spot bump ε = spot × 10⁻³ for delta/gamma; vol bump ε = 10⁻³ for vega). Theta and rho are set to NaN.

Parameters:
Returns:

Named tuple; only delta, gamma, vega are finite. theta and rho are NaN.

Return type:

OptionGreeks

ferro_ta.analysis.options.digital_option_price(underlying, strike, rate, time_to_expiry, volatility, *, option_type='call', digital_type='cash_or_nothing', carry=0.0)[source]

Price a digital (binary) option under BSM.

Parameters:
  • underlying (ArrayLike | float) – Current underlying (spot) price.

  • strike (ArrayLike | float) – Option strike price.

  • rate (ArrayLike | float) – Risk-free rate (annualised, decimal).

  • time_to_expiry (ArrayLike | float) – Time to expiry in years.

  • volatility (ArrayLike | float) – Implied volatility (annualised, decimal).

  • option_type (str) – "call" (default) or "put".

  • digital_type (str) – "cash_or_nothing" (default) — pays 1 unit of cash if ITM at expiry; or "asset_or_nothing" — pays the underlying asset price.

  • carry (ArrayLike | float) – Continuous carry / dividend yield (annualised, decimal). Default 0.

Returns:

Option price. Returns a scalar when all inputs are scalars, or an array when any input is an array.

Return type:

float or NDArray[float64]

Notes

Closed-form BSM formulas:

Cash-or-nothing call: e^{−rT} · N(d₂)
Cash-or-nothing put:  e^{−rT} · N(−d₂)
Asset-or-nothing call: S · e^{−qT} · N(d₁)
Asset-or-nothing put:  S · e^{−qT} · N(−d₁)

Put-call parity for cash-or-nothing: call + put = e^{−rT}. Put-call parity for asset-or-nothing: call + put = S · e^{−qT}.

Invalid inputs (non-positive spot/strike, negative time or vol) return NaN.

ferro_ta.analysis.options.early_exercise_premium(underlying, strike, rate, time_to_expiry, volatility, *, option_type='call', carry=0.0)[source]

Early exercise premium: American price − European BSM price.

Represents the additional value an American option holder gains from the right to exercise before expiry. Always ≥ 0.

Parameters:
Returns:

Premium ≥ 0. Typically 0 for calls with no dividends.

Return type:

float or NDArray[float64]

Notes

For equity calls with zero carry (no dividends), early exercise is never optimal so the premium is ≈ 0. For puts (or calls on dividend-paying underlyings), the premium increases with in-the-moneyness, rate, and time to expiry.

ferro_ta.analysis.options.expected_move(spot, iv, days_to_expiry, trading_days_per_year=252.0)[source]

Expected ±1σ move over days_to_expiry calendar days.

Uses the log-normal approximation:

upper_move = spot × e^{+σ√(days/trading_days)} − spot
lower_move = spot × e^{−σ√(days/trading_days)} − spot
Parameters:
  • spot (float) – Current underlying price.

  • iv (float) – Implied volatility (annualised, decimal — e.g. 0.20 for 20 %).

  • days_to_expiry (float) – Number of calendar days until expiry.

  • trading_days_per_year (float) – Annualisation factor (default 252).

Returns:

(lower_move, upper_move) — signed absolute price changes from spot. lower_move < 0, upper_move > 0.

Return type:

tuple[float, float]

Notes

Because of log-normal skew, |upper_move| > |lower_move|.

Examples

>>> from ferro_ta.analysis.options import expected_move
>>> lower, upper = expected_move(100.0, 0.20, 30)
>>> round(upper, 2)
7.14
ferro_ta.analysis.options.extended_greeks(underlying, strike, rate, time_to_expiry, volatility, *, option_type='call', model='bsm', carry=0.0)[source]

Return vanna, volga, charm, speed, and color (second-order / cross Greeks).

All Greeks are computed via closed-form BSM formulas. Black-76 is not yet supported and returns NaN for all five values.

Parameters:
  • underlying (ArrayLike | float) – Current underlying (spot) price.

  • strike (ArrayLike | float) – Option strike price.

  • rate (ArrayLike | float) – Risk-free rate (annualised, decimal — e.g. 0.05 for 5 %).

  • time_to_expiry (ArrayLike | float) – Time to expiry in years.

  • volatility (ArrayLike | float) – Implied volatility (annualised, decimal).

  • option_type (str) – "call" (default) or "put".

  • model (str) – "bsm" (default). "black76" returns NaN for all fields.

  • carry (ArrayLike | float) – Continuous carry / dividend yield (annualised, decimal). Default 0.

Returns:

Named tuple with fields:

  • vanna — ∂Δ/∂σ: sensitivity of delta to a change in vol.

  • volga — ∂²V/∂σ² (vomma): sensitivity of vega to a change in vol.

  • charm — ∂Δ/∂t: daily rate of change in delta (theta of delta).

  • speed — ∂Γ/∂S: rate of change in gamma with respect to spot.

  • color — ∂Γ/∂t: daily rate of change in gamma.

Return type:

ExtendedGreeks

Notes

Inputs may be scalars or broadcastable arrays. When arrays are supplied each field of the returned ExtendedGreeks is an NDArray.

Closed-form expressions (BSM, zero-carry):

vanna = -e^{-qT} · φ(d₁) · d₂ / σ
volga = S · e^{-qT} · φ(d₁) · √T · d₁ · d₂ / σ
charm = -e^{-qT} · φ(d₁) · [2(r-q)T - d₂·σ·√T] / (2T·σ·√T)
speed = -Γ/S · (d₁/(σ√T) + 1)
color = -Γ · [r-q + d₁·σ/(2√T) + (2(r-q)T - d₂·σ√T)·d₁/(2T·σ√T)]
ferro_ta.analysis.options.garman_klass_vol(open, high, low, close, window=20, trading_days_per_year=252.0)[source]

Rolling Garman-Klass OHLC realized volatility estimator (annualised).

Extends Parkinson by incorporating the open-close return. ~7.4× more efficient than close-to-close. Does not handle overnight gaps.

Parameters:
  • open (ArrayLike) – Arrays of daily OHLC prices (same length, ≥ window).

  • high (ArrayLike) – Arrays of daily OHLC prices (same length, ≥ window).

  • low (ArrayLike) – Arrays of daily OHLC prices (same length, ≥ window).

  • close (ArrayLike) – Arrays of daily OHLC prices (same length, ≥ window).

  • window (int) – Rolling look-back period in bars (default 20).

  • trading_days_per_year (float) – Annualisation factor (default 252).

Returns:

Same length as close. First window - 1 values are NaN.

Return type:

NDArray[float64]

Notes

Per-bar contribution:

GK = 0.5·ln²(H/L) − (2·ln2 − 1)·ln²(C/O)

Reference: Garman, M.B. & Klass, M.J. (1980). “On the Estimation of Security Price Volatilities from Historical Data.” Journal of Business, 53(1).

ferro_ta.analysis.options.greeks(underlying, strike, rate, time_to_expiry, volatility, *, option_type='call', model='bsm', carry=0.0)[source]

Return delta, gamma, vega, theta, and rho.

Parameters:
  • underlying (ArrayLike | float)

  • strike (ArrayLike | float)

  • rate (ArrayLike | float)

  • time_to_expiry (ArrayLike | float)

  • volatility (ArrayLike | float)

  • option_type (str)

  • model (str)

  • carry (ArrayLike | float)

Return type:

OptionGreeks

ferro_ta.analysis.options.implied_volatility(price, underlying, strike, rate, time_to_expiry, *, option_type='call', model='bsm', carry=0.0, initial_guess=0.2, tolerance=1e-08, max_iterations=100)[source]

Invert option prices to implied volatility.

Parameters:
  • price (ArrayLike | float)

  • underlying (ArrayLike | float)

  • strike (ArrayLike | float)

  • rate (ArrayLike | float)

  • time_to_expiry (ArrayLike | float)

  • option_type (str)

  • model (str)

  • carry (ArrayLike | float)

  • initial_guess (ArrayLike | float)

  • tolerance (float)

  • max_iterations (int)

Return type:

float | NDArray[float64]

ferro_ta.analysis.options.iv_percentile(iv_series, window=252)[source]

Compute rolling IV percentile in Rust while preserving the legacy API.

Parameters:
  • iv_series (ArrayLike)

  • window (int)

Return type:

NDArray[float64]

ferro_ta.analysis.options.iv_rank(iv_series, window=252)[source]

Compute rolling IV rank in Rust while preserving the legacy API.

Parameters:
  • iv_series (ArrayLike)

  • window (int)

Return type:

NDArray[float64]

ferro_ta.analysis.options.iv_zscore(iv_series, window=252)[source]

Compute rolling IV z-score in Rust while preserving the legacy API.

Parameters:
  • iv_series (ArrayLike)

  • window (int)

Return type:

NDArray[float64]

ferro_ta.analysis.options.label_moneyness(strikes, reference_price, *, option_type='call')[source]

Label strikes as ITM, ATM, or OTM.

Parameters:
  • strikes (ArrayLike)

  • reference_price (float)

  • option_type (str)

Return type:

NDArray[object_]

ferro_ta.analysis.options.option_price(underlying, strike, rate, time_to_expiry, volatility, *, option_type='call', model='bsm', carry=0.0)[source]

Model-dispatched option price helper.

Parameters:
  • underlying (ArrayLike | float)

  • strike (ArrayLike | float)

  • rate (ArrayLike | float)

  • time_to_expiry (ArrayLike | float)

  • volatility (ArrayLike | float)

  • option_type (str)

  • model (str)

  • carry (ArrayLike | float)

Return type:

float | NDArray[float64]

ferro_ta.analysis.options.parkinson_vol(high, low, window=20, trading_days_per_year=252.0)[source]

Rolling Parkinson high-low realized volatility estimator (annualised).

~5× more efficient than close-to-close for diffusion processes. Does not account for drift or overnight gaps.

Parameters:
  • high (ArrayLike) – Arrays of daily high and low prices (same length, ≥ window).

  • low (ArrayLike) – Arrays of daily high and low prices (same length, ≥ window).

  • window (int) – Rolling look-back period in bars (default 20).

  • trading_days_per_year (float) – Annualisation factor (default 252).

Returns:

Same length as high. First window - 1 values are NaN.

Return type:

NDArray[float64]

Notes

Formula per window:

σ² = (1 / (4·ln2·window)) · Σ ln²(Hᵢ/Lᵢ) × trading_days_per_year

Reference: Parkinson, M. (1980). “The Extreme Value Method for Estimating the Variance of the Rate of Return.” Journal of Business, 53(1).

ferro_ta.analysis.options.put_call_parity_deviation(call_price, put_price, spot, strike, rate, time_to_expiry, *, carry=0.0)[source]

Put-call parity deviation: C − P − (S·e^{−q·T} − K·e^{−r·T}).

At no-arbitrage the deviation is exactly 0. A non-zero result indicates mispricing, a data error, or a stale quote.

Parameters:
  • call_price (float) – Market or model price of the call option.

  • put_price (float) – Market or model price of the put option.

  • spot (float) – Current underlying price.

  • strike (float) – Common strike price of the call and put.

  • rate (float) – Risk-free rate (annualised, decimal).

  • time_to_expiry (float) – Time to expiry in years.

  • carry (float) – Continuous dividend yield / carry rate (annualised, decimal).

Returns:

Signed deviation. Positive → call is overpriced relative to put; negative → put is overpriced relative to call.

Return type:

float

Examples

>>> from ferro_ta.analysis.options import option_price, put_call_parity_deviation
>>> call = option_price(100, 100, 0.05, 1.0, 0.2, option_type="call")
>>> put  = option_price(100, 100, 0.05, 1.0, 0.2, option_type="put")
>>> put_call_parity_deviation(call, put, 100, 100, 0.05, 1.0)  # ≈ 0.0
ferro_ta.analysis.options.rogers_satchell_vol(open, high, low, close, window=20, trading_days_per_year=252.0)[source]

Rolling Rogers-Satchell OHLC realized volatility estimator (annualised).

Drift-invariant: unbiased for assets with non-zero expected return. Does not handle overnight gaps.

Parameters:
  • open (ArrayLike) – Arrays of daily OHLC prices (same length, ≥ window).

  • high (ArrayLike) – Arrays of daily OHLC prices (same length, ≥ window).

  • low (ArrayLike) – Arrays of daily OHLC prices (same length, ≥ window).

  • close (ArrayLike) – Arrays of daily OHLC prices (same length, ≥ window).

  • window (int) – Rolling look-back period in bars (default 20).

  • trading_days_per_year (float) – Annualisation factor (default 252).

Returns:

Same length as close. First window - 1 values are NaN.

Return type:

NDArray[float64]

Notes

Per-bar contribution (u = ln(H/O), d = ln(L/O), c = ln(C/O)):

RS = u·(u − c) + d·(d − c)

Reference: Rogers, L.C.G. & Satchell, S.E. (1991). “Estimating Variance from High, Low and Closing Prices.” Annals of Applied Probability, 1(4).

ferro_ta.analysis.options.select_strike(strikes, reference_price, *, option_type='call', selector='ATM', delta_target=None, volatilities=None, time_to_expiry=None, model='bsm', rate=0.0, carry=0.0)[source]

Select a strike by ATM/ITM/OTM offset or delta target.

Parameters:
  • strikes (ArrayLike)

  • reference_price (float)

  • option_type (str)

  • selector (str)

  • delta_target (float | None)

  • volatilities (ArrayLike | None)

  • time_to_expiry (float | None)

  • model (str)

  • rate (float)

  • carry (float)

Return type:

float | None

ferro_ta.analysis.options.smile_metrics(strikes, vols, reference_price, time_to_expiry, *, model='bsm', rate=0.0, carry=0.0)[source]

Compute ATM IV, 25-delta RR/BF, skew slope, and convexity.

Parameters:
  • strikes (ArrayLike)

  • vols (ArrayLike)

  • reference_price (float)

  • time_to_expiry (float)

  • model (str)

  • rate (float)

  • carry (float)

Return type:

SmileMetrics

ferro_ta.analysis.options.term_structure_slope(tenors, atm_ivs)[source]

Slope of ATM IV against tenor.

Parameters:
  • tenors (ArrayLike)

  • atm_ivs (ArrayLike)

Return type:

float

ferro_ta.analysis.options.vol_cone(close, *, windows=(21, 42, 63, 126, 252), trading_days_per_year=252.0)[source]

Historical realised vol distribution across window lengths (volatility cone).

For each window, computes the full history of rolling close-to-close realised vol, then returns the min / p25 / median / p75 / max distribution. Contextualises current implied vol: “Is 30 % IV cheap or expensive?”

Parameters:
  • close (ArrayLike) – Array of closing prices (length ≥ max(windows) + 1).

  • windows (tuple[int, ...]) – Tuple of rolling window sizes in bars. Default (21, 42, 63, 126, 252) (approx. 1 month, 2 months, 3 months, 6 months, 1 year).

  • trading_days_per_year (float) – Annualisation factor (default 252).

Returns:

Dataclass with arrays windows, min, p25, median, p75, max — one value per element of windows.

Return type:

VolCone

Notes

Uses close-to-close vol internally. Overlay the current IV on the cone to see whether it is historically cheap or expensive for each tenor.

Examples

>>> import numpy as np
>>> from ferro_ta.analysis.options import vol_cone
>>> rng = np.random.default_rng(0)
>>> close = 100 * np.cumprod(np.exp(rng.normal(0, 0.01, 500)))
>>> cone = vol_cone(close, windows=(21, 63, 252))
>>> cone.median  # annualised median realised vol per window
ferro_ta.analysis.options.yang_zhang_vol(open, high, low, close, window=20, trading_days_per_year=252.0)[source]

Rolling Yang-Zhang OHLC realized volatility estimator (annualised).

The most efficient standard estimator (~14× vs close-to-close). Handles overnight gaps by combining overnight, intraday open-close, and Rogers-Satchell variance components with an optimal weight k.

Parameters:
  • open (ArrayLike) – Arrays of daily OHLC prices (same length, ≥ window + 1).

  • high (ArrayLike) – Arrays of daily OHLC prices (same length, ≥ window + 1).

  • low (ArrayLike) – Arrays of daily OHLC prices (same length, ≥ window + 1).

  • close (ArrayLike) – Arrays of daily OHLC prices (same length, ≥ window + 1).

  • window (int) – Rolling look-back period in bars (default 20).

  • trading_days_per_year (float) – Annualisation factor (default 252).

Returns:

Same length as close. First window values are NaN.

Return type:

NDArray[float64]

Notes

Mixed estimator:

σ²_YZ = σ²_overnight + k·σ²_open_close + (1−k)·σ²_RS

where k = 0.34 / (1.34 + (window+1)/(window-1)).

Reference: Yang, D. & Zhang, Q. (2000). “Drift-Independent Volatility Estimation Based on High, Low, Open, and Close Prices.” Journal of Business, 73(3).

ferro_ta.analysis.futures — Futures and forward-curve analytics.

class ferro_ta.analysis.futures.CurveSummary(front_basis: 'float', average_basis: 'float', slope: 'float', is_contango: 'bool')[source]

Bases: object

Parameters:
average_basis: float
front_basis: float
is_contango: bool
slope: float
to_dict()[source]
Return type:

dict[str, float | bool]

ferro_ta.analysis.futures.annualized_basis(spot, future, time_to_expiry)[source]
Parameters:
Return type:

float

ferro_ta.analysis.futures.back_adjusted_continuous_contract(front, next_contract, next_weights)[source]
Parameters:
  • front (ArrayLike)

  • next_contract (ArrayLike)

  • next_weights (ArrayLike)

Return type:

NDArray[float64]

ferro_ta.analysis.futures.basis(spot, future)[source]
Parameters:
Return type:

float

ferro_ta.analysis.futures.calendar_spreads(futures_prices)[source]
Parameters:

futures_prices (ArrayLike)

Return type:

NDArray[float64]

ferro_ta.analysis.futures.carry_spread(spot, future, rate, time_to_expiry)[source]
Parameters:
Return type:

float

ferro_ta.analysis.futures.curve_slope(tenors, futures_prices)[source]
Parameters:
  • tenors (ArrayLike)

  • futures_prices (ArrayLike)

Return type:

float

ferro_ta.analysis.futures.curve_summary(spot, tenors, futures_prices)[source]
Parameters:
  • spot (float)

  • tenors (ArrayLike)

  • futures_prices (ArrayLike)

Return type:

CurveSummary

ferro_ta.analysis.futures.implied_carry_rate(spot, future, time_to_expiry)[source]
Parameters:
Return type:

float

ferro_ta.analysis.futures.parity_gap(call_price, put_price, spot, strike, rate, time_to_expiry, *, carry=0.0)[source]
Parameters:
Return type:

float

ferro_ta.analysis.futures.ratio_adjusted_continuous_contract(front, next_contract, next_weights)[source]
Parameters:
  • front (ArrayLike)

  • next_contract (ArrayLike)

  • next_weights (ArrayLike)

Return type:

NDArray[float64]

ferro_ta.analysis.futures.roll_yield(front_price, next_price, time_to_expiry)[source]
Parameters:
Return type:

float

ferro_ta.analysis.futures.synthetic_forward(call_price, put_price, strike, rate, time_to_expiry)[source]
Parameters:
Return type:

float

ferro_ta.analysis.futures.synthetic_spot(call_price, put_price, strike, rate, time_to_expiry, *, carry=0.0)[source]
Parameters:
Return type:

float

ferro_ta.analysis.futures.weighted_continuous_contract(front, next_contract, next_weights)[source]
Parameters:
  • front (ArrayLike)

  • next_contract (ArrayLike)

  • next_weights (ArrayLike)

Return type:

NDArray[float64]

ferro_ta.analysis.options_strategy — Typed strategy parameter schemas.

class ferro_ta.analysis.options_strategy.DerivativesStrategy(name: 'str', preset: 'LegPreset | str' = <LegPreset.CUSTOM: 'custom'>, legs: 'tuple[StrategyLeg, ...]'=<factory>, risk_controls: 'RiskControl' = <factory>, risk_mode: 'RiskMode | str' = <RiskMode.COMBINED_PNL: 'combined_pnl'>, commission: 'float' = 0.0, slippage: 'float' = 0.0, spread_assumption: 'float' = 0.0, limits: 'SimulationLimits' = <factory>)[source]

Bases: object

Parameters:
commission: float = 0.0
legs: tuple[StrategyLeg, ...]
limits: SimulationLimits
name: str
preset: LegPreset | str = 'custom'
risk_controls: RiskControl
risk_mode: RiskMode | str = 'combined_pnl'
slippage: float = 0.0
spread_assumption: float = 0.0
to_dict()[source]
Return type:

dict[str, Any]

class ferro_ta.analysis.options_strategy.ExpirySelector(kind: 'ExpirySelectorKind | str', explicit_date: 'date | None' = None)[source]

Bases: object

Parameters:
explicit_date: date | None = None
kind: ExpirySelectorKind | str
class ferro_ta.analysis.options_strategy.ExpirySelectorKind(*values)[source]

Bases: str, Enum

CURRENT_MONTH = 'current_month'
CURRENT_WEEK = 'current_week'
EXPLICIT_DATE = 'explicit_date'
NEXT_MONTH = 'next_month'
NEXT_WEEK = 'next_week'
class ferro_ta.analysis.options_strategy.LegPreset(*values)[source]

Bases: str, Enum

BEAR_PUT_SPREAD = 'bear_put_spread'
BULL_CALL_SPREAD = 'bull_call_spread'
CUSTOM = 'custom'
IRON_CONDOR = 'iron_condor'
STRADDLE = 'straddle'
STRANGLE = 'strangle'
class ferro_ta.analysis.options_strategy.RiskControl(stop_loss_type: 'str | None' = None, stop_loss_value: 'float | None' = None, target_type: 'str | None' = None, target_value: 'float | None' = None, trailstop_type: 'str | None' = None, trailstop_value: 'float | None' = None, breakeven_trigger: 'float | None' = None)[source]

Bases: object

Parameters:
  • stop_loss_type (str | None)

  • stop_loss_value (float | None)

  • target_type (str | None)

  • target_value (float | None)

  • trailstop_type (str | None)

  • trailstop_value (float | None)

  • breakeven_trigger (float | None)

breakeven_trigger: float | None = None
stop_loss_type: str | None = None
stop_loss_value: float | None = None
target_type: str | None = None
target_value: float | None = None
trailstop_type: str | None = None
trailstop_value: float | None = None
class ferro_ta.analysis.options_strategy.RiskMode(*values)[source]

Bases: str, Enum

COMBINED_PNL = 'combined_pnl'
PER_LEG = 'per_leg'
class ferro_ta.analysis.options_strategy.SimulationLimits(max_premium_outlay: 'float | None' = None, max_loss_per_trade: 'float | None' = None, daily_max_drawdown: 'float | None' = None, cooldown_bars: 'int' = 0, reentry_allowed: 'bool' = True)[source]

Bases: object

Parameters:
  • max_premium_outlay (float | None)

  • max_loss_per_trade (float | None)

  • daily_max_drawdown (float | None)

  • cooldown_bars (int)

  • reentry_allowed (bool)

cooldown_bars: int = 0
daily_max_drawdown: float | None = None
max_loss_per_trade: float | None = None
max_premium_outlay: float | None = None
reentry_allowed: bool = True
class ferro_ta.analysis.options_strategy.StrategyLeg(underlying: 'str', expiry_selector: 'ExpirySelector | None', strike_selector: 'StrikeSelector | None', option_type: 'str | None', side: 'str' = 'long', quantity: 'int' = 1, instrument: 'str' = 'option', premium_limit: 'float | None' = None)[source]

Bases: object

Parameters:
expiry_selector: ExpirySelector | None
instrument: str = 'option'
option_type: str | None
premium_limit: float | None = None
quantity: int = 1
side: str = 'long'
strike_selector: StrikeSelector | None
underlying: str
class ferro_ta.analysis.options_strategy.StrikeSelector(kind: 'StrikeSelectorKind | str', steps: 'int' = 0, delta: 'float | None' = None, explicit_strike: 'float | None' = None)[source]

Bases: object

Parameters:
delta: float | None = None
explicit_strike: float | None = None
kind: StrikeSelectorKind | str
steps: int = 0
class ferro_ta.analysis.options_strategy.StrikeSelectorKind(*values)[source]

Bases: str, Enum

ATM = 'atm'
DELTA = 'delta'
EXPLICIT = 'explicit'
ITM = 'itm'
OTM = 'otm'
ferro_ta.analysis.options_strategy.build_strategy_preset(preset, *, name, underlying, expiry_selector, base_strike_selector=None, risk_controls=None, risk_mode=RiskMode.COMBINED_PNL, commission=0.0, slippage=0.0, spread_assumption=0.0, limits=None)[source]

Build a common research preset using typed leg definitions.

Parameters:
Return type:

DerivativesStrategy

ferro_ta.analysis.derivatives_payoff — Multi-leg payoff and Greeks aggregation.

class ferro_ta.analysis.derivatives_payoff.OptionStrategy(legs)

Bases: object

Multi-leg option strategy: exact breakevens, max profit / loss (with unlimited detection), probability of profit, greeks and scenario grids.

Build with OptionStrategy(legs) or option_strategy(legs) (which also accepts dicts and a DerivativesStrategy).

breakevens()

Spots where expiry P&L crosses zero, ascending.

greeks(spot, rate=0.0, carry=0.0)

(delta, gamma, vega, theta, rho) summed over legs.

max_loss()

(value, spot) of the worst expiry P&L; (-inf, None) if unlimited.

max_profit()

(value, spot) of the best expiry P&L; (inf, None) if unlimited.

payoff(spot_grid)

Expiry P&L over spot_grid.

probability_of_profit(spot, volatility, time_to_expiry, rate=0.0, carry=0.0)

Risk-neutral probability that expiry P&L > 0 (lognormal underlying).

scenario_grid(spots, vol_shifts, days_forward=0.0, rate=0.0, carry=0.0)

Mark-to-model P&L, shape (len(vol_shifts), len(spots)).

class ferro_ta.analysis.derivatives_payoff.PayoffLeg(instrument: 'str', side: 'str', quantity: 'float' = 1.0, option_type: 'str | None' = None, strike: 'float | None' = None, premium: 'float' = 0.0, entry_price: 'float | None' = None, volatility: 'float | None' = None, time_to_expiry: 'float | None' = None, rate: 'float' = 0.0, carry: 'float' = 0.0, multiplier: 'float' = 1.0)[source]

Bases: object

Parameters:
carry: float = 0.0
entry_price: float | None = None
instrument: str
multiplier: float = 1.0
option_type: str | None = None
premium: float = 0.0
quantity: float = 1.0
rate: float = 0.0
side: str
strike: float | None = None
time_to_expiry: float | None = None
volatility: float | None = None
ferro_ta.analysis.derivatives_payoff.aggregate_greeks(spot, *, legs=None, strategy=None)[source]

Aggregate Greeks across option and futures legs.

Parameters:
Return type:

OptionGreeks

ferro_ta.analysis.derivatives_payoff.futures_leg_payoff(spot_grid, *, entry_price, side='long', quantity=1.0, multiplier=1.0)[source]

P/L profile for a futures leg.

Parameters:
  • spot_grid (ArrayLike)

  • entry_price (float)

  • side (str)

  • quantity (float)

  • multiplier (float)

Return type:

NDArray[float64]

ferro_ta.analysis.derivatives_payoff.option_leg_payoff(spot_grid, *, strike, premium=0.0, option_type='call', side='long', quantity=1.0, multiplier=1.0)[source]

Expiry payoff for a single option leg.

Parameters:
Return type:

NDArray[float64]

ferro_ta.analysis.derivatives_payoff.option_strategy(legs=None, *, strategy=None)[source]

Build an OptionStrategy from legs (PayoffLeg or dicts).

The result exposes exact breakevens(), max_profit() / max_loss() as (value, spot) with (±inf, None) when unlimited, probability_of_profit(spot, volatility, time_to_expiry), greeks(spot) and scenario_grid(spots, vol_shifts, days_forward). Option legs that should feed greeks or scenarios need volatility and time_to_expiry; per-leg rate/carry are ignored in favour of the method arguments.

Parameters:
Return type:

OptionStrategy

ferro_ta.analysis.derivatives_payoff.stock_leg_payoff(spot_grid, *, entry_price, side='long', quantity=1.0, multiplier=1.0)[source]

P/L profile for a single stock (equity) leg over a spot grid.

Payoff is linear:

P/L = sign(side) × quantity × multiplier × (spot − entry_price)

Mathematically equivalent to a futures leg — no optionality. Use this leg type when modelling strategies that hold the underlying equity: Covered Call, Protective Put, Collar, Covered Strangle, etc.

Parameters:
  • spot_grid (ArrayLike) – 1-D array of spot prices at which to evaluate the P/L.

  • entry_price (float) – Purchase (or short-sale) price of the stock.

  • side (str) – "long" (default) or "short".

  • quantity (float) – Number of shares / contracts (default 1).

  • multiplier (float) – Contract multiplier (default 1.0).

Returns:

P/L at each grid point, same shape as spot_grid.

Return type:

NDArray[float64]

ferro_ta.analysis.derivatives_payoff.strategy_payoff(spot_grid, *, legs=None, strategy=None)[source]

Aggregate expiry payoff across option and futures legs.

Parameters:
Return type:

NDArray[float64]

ferro_ta.analysis.derivatives_payoff.strategy_value(spot_grid, *, legs, time_to_expiry, volatility, rate=0.0, carry=0.0)[source]

Current BSM mid-price value of a multi-leg strategy over a spot grid.

Unlike strategy_payoff() (which computes intrinsic value at expiry), this uses live BSM pricing for option legs so the result reflects the pre-expiry value including time value.

Parameters:
  • spot_grid (ArrayLike) – Array of spot prices to evaluate.

  • legs (Sequence[PayoffLeg | Mapping[str, Any]]) – Sequence of PayoffLeg (or dicts). Option legs must have strike and premium set; future/stock legs must have entry_price set.

  • time_to_expiry (float) – Shared time-to-expiry (years) applied to all option legs.

  • volatility (float) – Shared implied vol applied to all option legs.

  • rate (float) – Risk-free rate applied to all legs.

  • carry (float) – Carry / dividend yield applied to all option legs.

Return type:

NDArray[float64]