Maximum adverse excursion (MAE) is the worst marked price movement against a trade between entry and exit. Maximum favorable excursion (MFE) is the best marked price movement in its favor over the same interval. They describe the path of one trade, while maximum drawdown describes a decline in an account or return series.
John Sweeney systematized the technique in the 1997 book Maximum Adverse Excursion. MAE and MFE can reveal path behavior hidden by final profit and loss, but they do not prove that a different stop or exit would have improved the strategy.
Definitions for long and short trades
For a long trade with entry price P_entry, lowest eligible price P_low, and highest eligible price P_high, a common return convention is:
MAE_long = min(0, P_low / P_entry - 1)
MFE_long = max(0, P_high / P_entry - 1)
For a short trade, adverse and favorable directions reverse:
MAE_short = min(0, (P_entry - P_high) / P_entry)
MFE_short = max(0, (P_entry - P_low) / P_entry)
Some libraries use a different denominator for short returns. Dollar excursion also multiplies price movement by position size, while risk-unit excursion divides by an initial stop distance or another risk budget. State the convention before comparing results.
MAE is usually reported as zero or negative and MFE as zero or positive. A trade that never moves below a long entry has zero MAE. Final return can be positive even after a large adverse excursion, and it can be negative after a large favorable excursion.
Which prices belong inside the trade
The answer depends on fill timing:
- A long position normally uses eligible lows for MAE and eligible highs for MFE. A short position reverses them.
- If entry fills at a bar's close, that bar's earlier high and low happened before the trade existed and should not count.
- If exit fills at a bar's open, later extremes on that bar should not count. If it fills at the close, they may count.
- Close-only data measures close-to-close excursion and can miss intrabar risk visible in high and low data.
- OHLC bars reveal extrema but not their order. If a stop and target both fall inside one bar, the bar alone cannot establish which triggered first.
- Bid, ask, spread, gaps, latency, partial fills, and market impact can make executable excursion worse than mid-price or bar excursion.
These rules should match the simulator's order and fill assumptions. Otherwise, MAE and MFE can describe prices the position never actually experienced.
VectorBT PRO example
The following two long trades use explicit OHLC data. Entries fill at the entry close. Exits fill at the exit close, so exit_price_close=True includes the exit bar's high and low. Position size is one unit, fees and slippage are zero, and the prices are synthetic.
import pandas as pd
import vectorbtpro as vbt
index = pd.date_range("2025-01-06", periods=8, freq="D")
open_ = pd.Series([100, 101, 98, 104, 103, 102, 99, 101], index=index)
high = pd.Series([101, 103, 100, 105, 104, 103, 102, 102], index=index)
low = pd.Series([99, 97, 94, 103, 101, 98, 96, 100], index=index)
close = pd.Series([100, 101, 98, 104, 103, 102, 99, 101], index=index)
entries = pd.Series(
[True, False, False, False, True, False, False, False],
index=index,
)
exits = pd.Series(
[False, False, False, True, False, False, False, True],
index=index,
)
pf = vbt.PF.from_signals(
close,
entries,
exits,
open=open_,
high=high,
low=low,
init_cash=10_000,
size=1,
)
trades = pf.trades.records_readable[
["Entry Index", "Exit Index", "Return"]
].copy()
trades["MAE return"] = pf.trades.get_mae(
use_returns=True,
exit_price_close=True,
).values
trades["MFE return"] = pf.trades.get_mfe(
use_returns=True,
exit_price_close=True,
).values
print(trades.to_string(index=False, float_format=lambda x: f"{x:.2%}"))
Entry Index Exit Index Return MAE return MFE return
2025-01-06 2025-01-09 4.00% -6.00% 5.00%
2025-01-10 2025-01-13 -1.94% -6.80% 0.00%
The first trade enters at 100, reaches a low of 94 and a high of 105, then exits at 104. Its final 4% return hides both the 6% adverse move and the 5% favorable peak. The second enters at 103, reaches a low of 96, never trades above its entry after the close fill, and exits at 101.
This example measures marked price excursion. get_mae() and get_mfe() do not add commissions or slippage to those excursion values. A risk report should show costs and executable-price assumptions separately.
What excursion analysis can answer
Across many comparable trades, MAE and MFE help investigate:
- How much adverse movement winners and losers experienced before exit.
- How much favorable movement was given back before the realized exit.
- Whether excursion changes with direction, instrument, volatility regime, holding period, or entry type.
- Whether a strategy's assumed stop distance is small relative to ordinary intratrade movement.
- Whether data resolution materially changes observed extremes.
Useful views include MAE versus final return, MFE versus final return, MFE minus realized return, excursion by trade age, and quantiles split by outcome or regime. Normalize by entry price, volatility, or initial risk when comparing instruments or differently sized trades. Dollar MAE alone can mostly reflect position size.
Why MAE does not identify an optimal stop by itself
A completed trade's MAE is known only after the path occurs. Choosing a stop from the same trades and then claiming the stopped result as evidence uses hindsight. A stop also changes more than the exit price: it can change later entries, capital availability, fees, slippage, and portfolio exposure.
Treat a candidate stop as a new strategy rule. Re-run the full chronological simulation with intrabar ambiguity handled explicitly, include costs and gaps, select the rule only on development data, and evaluate it on unseen periods. Check the whole return distribution rather than assuming that removing trades with large historical MAE preserves the profitable trades.
MAE also omits risks outside the observed holding interval. It does not measure portfolio drawdown, simultaneous exposure, margin calls, unfilled stop orders, or losses beyond the sampled high and low.
Tool support
VectorBT PRO exposes trade-level best and worst prices, their indices, MAE, MFE, expanding excursion paths, plots against PnL, and volatility-normalized edge-ratio calculations. The current implementation can use open, high, low, and close arrays and has explicit controls for entry and exit bar inclusion. Its edge ratio is based on mean volatility-normalized favorable and adverse excursion, not simply one trade's raw MFE / MAE.
Backtesting.py exposes entry bar, exit bar, size, direction, prices, and PnL in its trade records. Those fields are enough to slice the matching OHLC interval and calculate excursion, but MAE and MFE are not standard columns in its main trade table. Whichever tool performs the calculation, preserve the exact interval, direction, price field, cost convention, and open-trade policy so the statistic remains reproducible.