Skip to content

factrix.metrics.mfe_mae

MFE/MAE — per-event price path excursion analysis.

Answers: "what does the price path look like after events?"

Requires bar-by-bar price data within the event window. If price is not available, compute_mfe_mae returns an empty DataFrame and mfe_mae_summary returns a short-circuit MetricOutput (value=NaN, metadata["reason"]) — never None.

Metrics

compute_mfe_mae — per-event MFE/MAE/Bars_to_MFE/Bars_to_MAE mfe_mae_summary — aggregate summary (p50, p75, ratio)

Notes

Pipeline. Per-event MFE / MAE excursion over a fixed window (per-event step), then cross-event quantile / ratio summary; descriptive (no formal H₀).

factrix.metrics.mfe_mae.compute_mfe_mae

compute_mfe_mae(df: DataFrame, *, window: int = 20, estimation_window: int = 60, min_estimation_samples: int = DEFAULT_MIN_ESTIMATION_SAMPLES, factor_col: str = 'factor', price_col: str = 'price') -> DataFrame

Per-event Maximum Favorable/Adverse Excursion.

For each event (\(\text{factor} \neq 0\)), examines the window subsequent bars to find the peak gain (MFE) and peak loss (MAE) relative to event entry price, adjusted for signal direction.

Also reports an estimation-window-normalised z-score per event:

\[ z_{\mathrm{mfe}} = \mathrm{mfe} / (\hat\sigma \cdot \sqrt{W}), \quad z_{\mathrm{mae}} = \mathrm{mae} / (\hat\sigma \cdot \sqrt{W}) \]

where \(\hat\sigma\) is the daily-return std over the estimation_window bars preceding the event. MFE/MAE are order statistics whose expected magnitude grows as \(\sqrt{W \cdot \sigma^2}\); comparing raw MFE across horizons or vol regimes conflates time-scale with signal strength. The z-scored versions are the apples-to-apples quantity for cross-setup comparisons (Campbell-Lo-MacKinlay (1997) Ch 4 on horizon scaling of order statistics).

Parameters:

Name Type Description Default
df DataFrame

Panel with date, asset_id, factor, price.

required
window int

Number of bars after event to examine. Maps to EventConfig.event_window_post.

20
estimation_window int

Look-back window for per-event daily-return σ (default 60).

60
min_estimation_samples int

Minimum non-degenerate prior bars required to produce a finite est_sigma. Default 20 mirrors the BMP (Boehmer-Musumeci-Poulsen (1991)) daily-σ convention; weekly panels can drop to ~8-10. Below the threshold, mfe_z / mae_z report NaN. Must be ≥2 (the std degrees-of-freedom floor).

DEFAULT_MIN_ESTIMATION_SAMPLES
factor_col str

Event signal column.

'factor'
price_col str

Price column for bar-by-bar path.

'price'

Returns:

Type Description
DataFrame

DataFrame with ``date, asset_id, mfe, mae, mfe_z, mae_z,

DataFrame

est_sigma, bars_to_mfe, bars_to_mae``. Empty DataFrame if

DataFrame

price_col not present.

Notes

For each event with entry price \(P_0 = \text{price}[t_{\text{event}}]\) and post-event window \(P_{1 \ldots W}\):

\[ \begin{aligned} r_k &= \text{direction} \cdot (P_k / P_0 - 1) \quad \text{for } k = 1 \ldots W \\ \mathrm{mfe} &= \max_k r_k, \quad \mathrm{mae} = \min_k r_k \\ \hat\sigma &= \mathrm{std}(r_d) \text{ over the prior estimation window} \\ z_{\mathrm{mfe}} &= \mathrm{mfe} / (\hat\sigma \cdot \sqrt{W}) \\ z_{\mathrm{mae}} &= \mathrm{mae} / (\hat\sigma \cdot \sqrt{W}) \end{aligned} \]

factrix scales by \(\sqrt{W}\) because MFE/MAE are order statistics whose expected magnitude grows as \(\sqrt{W \cdot \sigma^2}\) — comparing raw MFE across horizons or vol regimes conflates time-scale with signal strength. \(\hat\sigma\) excludes the event-day bar to avoid feeding the signal back into its own denominator.

Examples:

>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.mfe_mae import compute_mfe_mae
>>> panel = compute_forward_return(
...     fx.datasets.make_event_panel(n_assets=50, n_dates=400, seed=0),
...     forward_periods=5,
... )
>>> per_event = compute_mfe_mae(panel, window=20)
>>> set(per_event.columns) >= {"date", "asset_id", "mfe", "mae"}
True

factrix.metrics.mfe_mae.mfe_mae_summary

mfe_mae_summary(mfe_mae_df: DataFrame) -> MetricOutput

Aggregate MFE/MAE statistics.

Reports MFE/MAE ratio as the primary value — higher is better (favorable excursion exceeds adverse excursion).

Parameters:

Name Type Description Default
mfe_mae_df DataFrame

Output of compute_mfe_mae().

required

Returns:

Type Description
MetricOutput

MetricOutput with value=MFE_p50/|MAE_p75| ratio. On insufficient

MetricOutput

data (empty input or fewer than MIN_EVENTS_HARD rows), returns a

MetricOutput

short-circuit MetricOutput (value=NaN, metadata["reason"]

MetricOutput

set) so all metrics share a single return contract.

Notes

Headline ratio = quantile(mfe, 0.50) / |quantile(mae, 0.75)|. Z-normalised siblings mfe_z_p50 / mae_z_p75 / mfe_mae_ratio_z are reported when mfe_z / mae_z are present and pass the same minimum-events threshold.

factrix pairs the MFE median against the MAE 75th percentile (not the median) because the asymmetric quantile pair captures risk-adjusted favourability: a strategy with median favourable excursion that exceeds typical adverse excursion in the worst quartile is the practically useful regime.

Examples:

Chain from :func:compute_mfe_mae output:

>>> import factrix as fx
>>> from factrix.preprocess import compute_forward_return
>>> from factrix.metrics.mfe_mae import compute_mfe_mae, mfe_mae_summary
>>> panel = compute_forward_return(
...     fx.datasets.make_event_panel(n_assets=50, n_dates=400, seed=0),
...     forward_periods=5,
... )
>>> per_event = compute_mfe_mae(panel, window=20)
>>> result = mfe_mae_summary(per_event)
>>> result.name
'mfe_mae_summary'

Use cases

  • Peak favourable vs adverse excursion


    For each event, find the peak gain (MFE) and peak loss (MAE) over the post-event window, plus bars-to-peak. Descriptive of the shape of the post-event price path, not just its endpoint.

  • Risk-adjusted favourability


    Headline ratio \(\mathrm{MFE}_{p50} / |\mathrm{MAE}_{p75}|\) pairs the median favourable excursion against the 75th percentile adverse excursion — captures whether typical upside exceeds worst- quartile downside.

  • Cross-horizon / cross-regime comparison


    Z-scored variants mfe_z / mae_z (divided by \(\hat\sigma \sqrt{W}\)) absorb the \(\sqrt{W \cdot \sigma^2}\) horizon scaling of order statistics — the apples-to-apples quantity for comparing event setups across windows or volatility regimes.

Choosing a function

Goal Function
Per-event MFE / MAE / bars-to-peak table for downstream cuts compute_mfe_mae
Aggregate distribution summary (quantiles, ratio, z-scored siblings) mfe_mae_summary

Worked example — per-event excursion then summary

compute_mfe_mae → mfe_mae_summary on a synthetic event panel

import factrix as fx
from factrix.metrics.mfe_mae import compute_mfe_mae, mfe_mae_summary

panel = fx.datasets.make_event_panel(
    n_assets=200, n_dates=500, event_rate=0.02,
    post_event_drift=0.004, with_price=True, seed=2024,
)

per_event = compute_mfe_mae(panel, window=20, estimation_window=60)
print(per_event.head())
# ┌────────────┬──────────┬────────┬─────────┬────────┬────────┐
# │ date       ┆ asset_id ┆  mfe   ┆  mae    ┆ mfe_z  ┆ mae_z  │
# ├────────────┼──────────┼────────┼─────────┼────────┼────────┤
# │ 2024-01-04 ┆ A0001    ┆ 0.031  ┆ -0.018  ┆  0.74  ┆ -0.43  │
# │  ...       ┆ ...      ┆  ...   ┆  ...    ┆  ...   ┆  ...   │
# └────────────┴──────────┴────────┴─────────┴────────┴────────┘

out = mfe_mae_summary(per_event)
print(out.value,
      out.metadata["mfe_p50"], out.metadata["mae_p75"],
      out.metadata.get("mfe_mae_ratio_z"))
# 1.27  0.024  -0.019  1.31   (approximate)

See also