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:
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 |
required |
window
|
int
|
Number of bars after event to examine. Maps to
|
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 |
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
|
|
Notes
For each event with entry price \(P_0 = \text{price}[t_{\text{event}}]\) and post-event window \(P_{1 \ldots W}\):
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 |
required |
Returns:
| Type | Description |
|---|---|
MetricOutput
|
MetricOutput with value=MFE_p50/|MAE_p75| ratio. On insufficient |
MetricOutput
|
data (empty input or fewer than |
MetricOutput
|
short-circuit MetricOutput ( |
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¶
-
event_around_return
Per-offset mean curve on the same post-event window — read for drift shape; MFE/MAE read for excursion magnitude.
-
caar/bmp_test
Inferential CAAR / BMP \(z\) on the endpoint of the same event window.
-
by_slice
Per-slice excursion summaries (regime / universe / sector).
-
Metric applicability reference
Event-window / estimation-window contracts and price-data requirements.
-
Individual × Sparse landing
Adjacent event-study metrics in the same cell.