Beyond realized PnL: The unseen cost of unharvested run-up

In quantitative strategy research and algorithmic backtesting, trading performance is predominantly summarized by realized trade outcomes: win rate, average win/loss ratio, and terminal profit and loss (PnL). A trade that enters at $100.00 and exits at $105.00 is logged in the trade ledger as a profitable +$500 win (assuming a standard 100-share allocation). However, auditing only the terminal state conceals the most critical dimension of trade management: the price path traversed between fill and liquidation.

Consider two hypothetical long trades executed with identical capital allocation and identical realized profits:

  1. Trade Alpha: Enters at $100.00, encounters modest volatility, reaches an intrabar high of $106.00, and is closed at $105.00 via an end-of-session signal (+5.00 points).
  2. Trade Beta: Enters at $100.00, accelerates aggressively into an explosive impulse wave peaking at $125.00 (+25.00 points), fails to harvest gains, slowly bleeds away 80% of its open paper equity during a multi-week consolidation, and finally triggers an exit at $105.00 (+5.00 points).

On a standard broker equity statement, Trade Alpha and Trade Beta appear identical: both delivered +$500. In reality, Trade Beta was an operational catastrophe. It exposed capital to extended market risk for weeks, tied up portfolio purchasing power, and surrendered $2,000 of open equity before liquidation.

Code
flowchart TD
    subgraph ExecutionLifecycle ["Trade Lifecycle Price Path"]
        Entry["Entry Fill (P_entry)"]
        AdverseDip["Adverse Dip (Peak Drawdown)"]
        FavorablePeak["Favorable Run-Up (Peak Paper Profit)"]
        TerminalExit["Terminal Exit Fill (P_exit)"]
    end

    Entry -->|"Adverse Movement"| AdverseDip
    AdverseDip -->|"Impulse Wave"| FavorablePeak
    FavorablePeak -->|"Reversal / Decay"| TerminalExit

    subgraph ForensicAudit ["Forensic Performance Metrics"]
        MAE["MAE: Peak Intrabar Risk Absorbed"]
        MFE["MFE: Peak Intrabar Potential Created"]
        ExitEff["Exit Efficiency: PnL / MFE Capture"]
    end

    AdverseDip -.->|"Quantifies Risk"| MAE
    FavorablePeak -.->|"Quantifies Opportunity"| MFE
    TerminalExit -.->|"Quantifies Harvest"| ExitEff

To quantify the peak paper profit attained by an open position before its close, John Sweeney introduced the concept of Maximum Favorable Excursion (MFE) in his foundational 1992 work Campaign Trading. In quantitative finance and algorithmic systems engineering, MFE measures the maximum unrealized gain (peak favorable run-up) experienced by a position throughout its active holding duration.

Evaluating MFE alongside realized returns enables algorithmic researchers to solve three vital engineering challenges:

  • Quantifying Exit Efficiency: Measuring the exact percentage of available trend run-up captured by the strategy rather than left on the table.
  • Eliminating Round-Trip Losses: Identifying positions that attained high favorable excursion (e.g. +2.0R to +3.0R) only to reverse completely into full stop-loss liquidations.
  • Calibrating Take-Profit and Trailing Stops: Establishing empirical, distribution-based profit targets and trailing stop triggers without falling prey to post-hoc curve-fitting.

While Maximum Adverse Excursion provides the defensive audit of initial trade risk and stop-loss placement, MFE provides the offensive audit of trade harvesting and profit preservation.


Mathematical formulation of MFE across long and short positions

To audit trade mechanics without ambiguity, MFE must be formulated rigorously across both long and short market directions. Like its counterpart MAE, MFE should be evaluated across three standard scales: absolute points/dollars, percentage gain relative to entry price, and initial risk-normalized units (R-multiples).

Directional price extremes

Let an executed trade enter at price P_entry at timestamp t_entry and close at price P_exit at timestamp t_exit. The holding period spans the discrete time intervals t in [t_entry, t_exit].

Code
Long Position MFE:
P_max = max(High_t)  for all t in [t_entry, t_exit]
MFE_$ = (P_max - P_entry) * Size
MFE_% = ((P_max - P_entry) / P_entry) * 100
MFE_R = (P_max - P_entry) / (P_entry - Stop_initial) = MFE_$ / Initial_Risk_$

Short Position MFE:
P_min = min(Low_t)   for all t in [t_entry, t_exit]
MFE_$ = (P_entry - P_min) * Size
MFE_% = ((P_entry - P_min) / P_entry) * 100
MFE_R = (P_entry - P_min) / (Stop_initial - P_entry) = MFE_$ / Initial_Risk_$

In systematic trading, normalizing MFE by initial risk Initial_Risk_$ = |P_entry - Stop_initial| * Size produces an R-multiple metric (MFE_R). Normalization ensures that trades taken across different volatility regimes, assets, or account equity scales can be compared on an identical statistical foundation.

Formulating exit efficiency

John Sweeney defined overall trade performance as a function of entry efficiency and exit efficiency. In modern quantitative strategy engineering, Exit Efficiency assesses how effectively the strategy captured the favorable excursion that the market made available.

Two distinct mathematical expressions are widely used in trading research:

1. Sweeney's classic high/low range efficiency

Sweeney formulated exit efficiency by comparing the exit price against the total price range spanned during the trade lifecycle:

Code
Long Sweeney Exit Efficiency (%):
Efficiency_Sweeney = ((P_exit - P_min) / (P_max - P_min)) * 100

Short Sweeney Exit Efficiency (%):
Efficiency_Sweeney = ((P_max - P_exit) / (P_max - P_min)) * 100

Where P_max and P_min represent the highest high and lowest low recorded over the trade duration. If a long position exits at the absolute peak P_max, Sweeney efficiency is 100%. If it exits at the absolute low of the trade lifecycle P_min, efficiency is 0%.

2. Run-up capture efficiency (Peak-relative net capture)

While Sweeney's metric is bounded between 0% and 100% on the total bar span, quantitative portfolio managers require an efficiency metric directly anchored to realized PnL and transaction friction:

Code
Run-Up Capture Efficiency (%) = (Realized Net PnL_$ / Gross MFE_$) * 100

Where:

  • Realized Net PnL_$ accounts for round-turn trading commissions, exchange fees, and bid-ask slippage.
  • Gross MFE_$ represents the peak theoretical dollar profit reachable at P_max (longs) or P_min (shorts).
Code
Transaction Cost Drag:
Realized Net PnL_$ = Realized Gross PnL_$ - (2 * Commission_Per_Share + 2 * Slippage_Per_Share) * Size

When transaction costs are incorporated, a trade with a modest gross run-up can exhibit negative exit efficiency even if the gross exit closed at a small gain. Recognizing frictional drag prevents researchers from deploying micro-scalping profit targets that look attractive in frictionless simulations but bleed out in live execution.


Intrabar pricing ambiguity and execution-aware path modeling

Calculating MFE from discrete OHLCV bars introduces subtle path-dependency hazards. Because bar data aggregates price activity into four boundary snapshots (Open, High, Low, Close), the exact chronological sequence of intrabar price movements is unrecorded.

Failing to resolve this ambiguity produces lookahead contamination and distorted MFE statistics.

Code
flowchart LR
    subgraph BarSequence ["Intrabar Event Ambiguity"]
        O["Open"]
        H["High (Peak MFE?)"]
        L["Low (Stop Trigger?)"]
        C["Close"]
    end

    O --> H --> L --> C
    O --> L --> H --> C

    note["Sequence Question: Did High occur before Low, or Low before High?"]

1. Entry bar sequencing: The CLOSE-fill pitfall

When a strategy executes an entry order at the CLOSE of a bar (a standard design in end-of-bar trend-following models), the bar's HIGH and LOW occurred before the trade existed.

  • Naive calculation: If a long trade enters at the close of bar $t$ at $105.00, and bar $t$'s high was $108.00, a naive script calculates an immediate MFE of $3.00. That $3.00 high occurred prior to entry. Attributing it to trade run-up is pure temporal leakage.
  • Execution-aware resolution: For CLOSE fills, the entry bar's contribution to post-entry excursion is strictly bounded at zero. Effective price tracking for MFE must begin at P_entry, incorporating only subsequent bar extremes (t + 1, t + 2, ...).

Conversely, when an order enters at the OPEN of a bar (such as next-bar market orders), the entire range [Low_t, High_t] unfolds after entry, making the full bar high or low legitimate excursion data.

2. Exit bar sequencing: Conservative vs. optimistic modeling

On the exit bar, if a trade is liquidated via a stop-loss or limit order, did the market touch the peak MFE before triggering the exit, or did it trigger the exit first before surging?

  • Optimistic Policy: Assumes the favorable extreme occurred before the exit trigger. On a stop-out bar, it logs the day's high as valid MFE.
  • Conservative Policy: If an exit is triggered intrabar (e.g. initial stop or trailing stop), the favorable excursion on the exit bar is capped at the exit fill price or the open, preventing phantom peaks from inflating run-up metrics.

In production backtesting engines like OpenBacktesting, bar execution policies enforce causal discipline: orders execute sequentially on incoming price ticks or strict bar rules, ensuring no unearned excursion is attributed to historical performance records.


Production Python implementation: Auditing trade run-up and efficiency

The following production-ready Python script audits MFE, Sweeney exit efficiency, and net run-up capture from discrete OHLCV bars and historical trade records. It enforces execution-aware entry and exit boundary handling and models round-turn transaction friction.

Code
import math
from dataclasses import dataclass
from typing import List, Optional, Literal

@dataclass(frozen=True)
class OHLCVBar:
    timestamp: str
    open: float
    high: float
    low: float
    close: float
    volume: float

@dataclass(frozen=True)
class TradeRecord:
    trade_id: str
    ticker: str
    direction: Literal["LONG", "SHORT"]
    entry_time: str
    entry_price: float
    exit_time: str
    exit_price: float
    size: float
    entry_exec: Literal["OPEN", "CLOSE", "INTRABAR"] = "OPEN"
    exit_exec: Literal["OPEN", "CLOSE", "STOP", "LIMIT", "TRAILING_STOP"] = "CLOSE"
    initial_stop: Optional[float] = None
    commission_per_share: float = 0.005
    slippage_per_share: float = 0.01

@dataclass(frozen=True)
class MFEAuditResult:
    trade_id: str
    direction: str
    entry_price: float
    exit_price: float
    size: float
    gross_pnl: float
    net_pnl: float
    realized_pct: float
    realized_r: Optional[float]
    mfe_dollar: float
    mfe_pct: float
    mfe_r: Optional[float]
    mae_dollar: float
    mae_pct: float
    mae_r: Optional[float]
    exit_efficiency_pct: float       # (Net Realized PnL / MFE Dollar) * 100
    sweeney_efficiency_pct: float    # Sweeney high/low range efficiency
    holding_bars: int
    classification: str

def audit_trade_mfe(
    trade: TradeRecord,
    bars: List[OHLCVBar],
    intrabar_policy: str = "execution_aware"
) -> MFEAuditResult:
    """
    Audit an individual trade lifecycle against underlying OHLCV bars.
    
    Calculates MFE, MAE, realized PnL, Sweeney exit efficiency, and net
    run-up capture efficiency while accounting for directional mechanics,
    intrabar execution timing, and transaction friction.
    """
    trade_bars = [b for b in bars if trade.entry_time <= b.timestamp <= trade.exit_time]
    if not trade_bars:
        raise ValueError(f"No bars found for trade {trade.trade_id} between {trade.entry_time} and {trade.exit_time}")

    n_bars = len(trade_bars)
    total_costs = (trade.commission_per_share * 2 + trade.slippage_per_share * 2) * trade.size

    # Establish risk unit R
    risk_per_unit = None
    if trade.initial_stop is not None:
        if trade.direction == "LONG":
            risk_per_unit = max(1e-6, trade.entry_price - trade.initial_stop)
        else:
            risk_per_unit = max(1e-6, trade.initial_stop - trade.entry_price)

    if trade.direction == "LONG":
        gross_pnl = (trade.exit_price - trade.entry_price) * trade.size
        net_pnl = gross_pnl - total_costs
        realized_pct = ((trade.exit_price - trade.entry_price) / trade.entry_price) * 100.0

        if intrabar_policy == "execution_aware" and trade.entry_exec == "CLOSE":
            # If entry occurred at the bar close, the bar's prior high/low cannot be claimed
            effective_highs = [trade.entry_price] + [b.high for b in trade_bars[1:]]
            effective_lows = [trade.entry_price] + [b.low for b in trade_bars[1:]]
        else:
            effective_highs = [b.high for b in trade_bars]
            effective_lows = [b.low for b in trade_bars]

        if trade.exit_exec == "LIMIT":
            effective_highs[-1] = max(trade.exit_price, effective_highs[-1])

        peak_high = max(effective_highs)
        trough_low = min(effective_lows)

        mfe_dollar = max(0.0, (peak_high - trade.entry_price) * trade.size)
        mfe_pct = max(0.0, ((peak_high - trade.entry_price) / trade.entry_price) * 100.0)

        mae_dollar = max(0.0, (trade.entry_price - trough_low) * trade.size)
        mae_pct = max(0.0, ((trade.entry_price - trough_low) / trade.entry_price) * 100.0)

        hl_span = peak_high - trough_low
        sweeney_eff = ((trade.exit_price - trough_low) / hl_span * 100.0) if hl_span > 1e-6 else 100.0

    else:  # SHORT
        gross_pnl = (trade.entry_price - trade.exit_price) * trade.size
        net_pnl = gross_pnl - total_costs
        realized_pct = ((trade.entry_price - trade.exit_price) / trade.entry_price) * 100.0

        if intrabar_policy == "execution_aware" and trade.entry_exec == "CLOSE":
            effective_highs = [trade.entry_price] + [b.high for b in trade_bars[1:]]
            effective_lows = [trade.entry_price] + [b.low for b in trade_bars[1:]]
        else:
            effective_highs = [b.high for b in trade_bars]
            effective_lows = [b.low for b in trade_bars]

        if trade.exit_exec == "LIMIT":
            effective_lows[-1] = min(trade.exit_price, effective_lows[-1])

        peak_high = max(effective_highs)
        trough_low = min(effective_lows)

        mfe_dollar = max(0.0, (trade.entry_price - trough_low) * trade.size)
        mfe_pct = max(0.0, ((trade.entry_price - trough_low) / trade.entry_price) * 100.0)

        mae_dollar = max(0.0, (peak_high - trade.entry_price) * trade.size)
        mae_pct = max(0.0, ((peak_high - trade.entry_price) / trade.entry_price) * 100.0)

        hl_span = peak_high - trough_low
        sweeney_eff = ((peak_high - trade.exit_price) / hl_span * 100.0) if hl_span > 1e-6 else 100.0

    # Risk-normalized metrics
    realized_r = (net_pnl / (risk_per_unit * trade.size)) if risk_per_unit else None
    mfe_r = (mfe_dollar / (risk_per_unit * trade.size)) if risk_per_unit else None
    mae_r = (mae_dollar / (risk_per_unit * trade.size)) if risk_per_unit else None

    # Run-up capture efficiency
    if mfe_dollar > 1e-6:
        exit_eff = (net_pnl / mfe_dollar) * 100.0
    else:
        exit_eff = 100.0 if net_pnl >= 0 else 0.0

    # Empirical trade taxonomy
    if net_pnl > 0:
        if exit_eff >= 75.0:
            classification = "Optimal Peak Capture"
        elif exit_eff >= 45.0:
            classification = "Decayed Trend Follower"
        else:
            classification = "Premature Run-Up Exit"
    else:
        if mfe_dollar > (trade.entry_price * 0.02 * trade.size):
            classification = "Round-Tripped Gain (Gave Back)"
        else:
            classification = "Disciplined Scratch/Loss"

    return MFEAuditResult(
        trade_id=trade.trade_id,
        direction=trade.direction,
        entry_price=trade.entry_price,
        exit_price=trade.exit_price,
        size=trade.size,
        gross_pnl=round(gross_pnl, 2),
        net_pnl=round(net_pnl, 2),
        realized_pct=round(realized_pct, 2),
        realized_r=round(realized_r, 2) if realized_r is not None else None,
        mfe_dollar=round(mfe_dollar, 2),
        mfe_pct=round(mfe_pct, 2),
        mfe_r=round(mfe_r, 2) if mfe_r is not None else None,
        mae_dollar=round(mae_dollar, 2),
        mae_pct=round(mae_pct, 2),
        mae_r=round(mae_r, 2) if mae_r is not None else None,
        exit_efficiency_pct=round(exit_eff, 1),
        sweeney_efficiency_pct=round(sweeney_eff, 1),
        holding_bars=n_bars,
        classification=classification
    )

if __name__ == "__main__":
    sample_bars = [
        OHLCVBar("2026-05-01", 100.0, 105.0,  99.0, 104.0, 1200000),
        OHLCVBar("2026-05-02", 104.5, 110.0, 103.5, 109.0, 1400000),
        OHLCVBar("2026-05-03", 109.0, 115.5, 108.0, 114.5, 1600000),
        OHLCVBar("2026-05-04", 114.0, 118.0, 113.0, 116.0, 1500000),
        OHLCVBar("2026-05-05", 116.0, 116.5, 111.0, 112.0, 1300000),
        OHLCVBar("2026-05-06", 112.0, 112.5, 107.0, 108.0, 1100000),
        OHLCVBar("2026-05-07", 108.0, 111.0, 106.5, 110.0, 1250000),
        OHLCVBar("2026-05-08", 110.5, 114.0, 109.5, 113.5, 1450000),
        OHLCVBar("2026-05-09", 113.5, 115.0, 106.0, 107.0, 1900000),
        OHLCVBar("2026-05-10", 106.5, 107.0,  98.0,  99.0, 2100000),
        OHLCVBar("2026-05-11",  99.0, 100.5,  94.5,  95.0, 1800000),
        OHLCVBar("2026-05-12",  95.0,  96.0,  90.0,  91.0, 2400000),
        OHLCVBar("2026-05-13",  91.5,  94.0,  88.5,  89.0, 2000000),
        OHLCVBar("2026-05-14",  89.0,  95.0,  88.5,  94.5, 2600000),
    ]

    sample_trades = [
        # T-01: Trend runner with disciplined limit take-profit near peak
        TradeRecord("T-01", "SYM", "LONG", "2026-05-01", 100.0, "2026-05-04", 117.0, 100, "OPEN", "LIMIT", initial_stop=96.0),
        # T-02: Trend follower closed via trailing stop after giving back 50% from high
        TradeRecord("T-02", "SYM", "LONG", "2026-05-01", 100.0, "2026-05-06", 108.0, 100, "OPEN", "TRAILING_STOP", initial_stop=96.0),
        # T-03: The Round-Tripper: Reached +11.0 pts MFE, failed to harvest, closed at a loss
        TradeRecord("T-03", "SYM", "LONG", "2026-05-02", 104.5, "2026-05-10", 99.0, 100, "OPEN", "STOP", initial_stop=98.0),
        # T-04: Short trade riding downward breakdown directly into limit target
        TradeRecord("T-04", "SYM", "SHORT", "2026-05-10", 106.5, "2026-05-13", 89.0, 100, "OPEN", "LIMIT", initial_stop=110.0),
        # T-05: Short entered at close, captured small profit before sharp counter-trend rebound
        TradeRecord("T-05", "SYM", "SHORT", "2026-05-11", 95.0, "2026-05-14", 94.5, 100, "CLOSE", "STOP", initial_stop=97.0),
    ]

    results = [audit_trade_mfe(t, sample_bars) for t in sample_trades]

    print(f"{'ID':<5} {'Dir':<5} {'Entry':<6} {'Exit':<6} {'Net PnL ($)':<12} {'MFE ($)':<9} {'ExitEff%':<9} {'SweenEff%':<10} {'Classification':<30}")
    print("-" * 95)
    for r in results:
        print(f"{r.trade_id:<5} {r.direction:<5} {r.entry_price:<6.1f} {r.exit_price:<6.1f} {r.net_pnl:+11.2f} {r.mfe_dollar:8.2f} {r.exit_efficiency_pct:8.1f}% {r.sweeney_efficiency_pct:9.1f}% {r.classification:<30}")

Script execution audit report

Executing the audit script across these trade records produces the following reconciliation ledger:

Code
ID    Dir   Entry  Exit   Net PnL ($)  MFE ($)   ExitEff%  SweenEff%  Classification                
-----------------------------------------------------------------------------------------------
T-01  LONG  100.0  117.0     +1697.00  1800.00     94.3%      94.7% Optimal Peak Capture          
T-02  LONG  100.0  108.0      +797.00  1800.00     44.3%      47.4% Premature Run-Up Exit         
T-03  LONG  104.5  99.0       -553.00  1350.00    -41.0%       5.0% Round-Tripped Gain (Gave Back)
T-04  SHORT 106.5  89.0      +1747.00  1800.00     97.1%      97.3% Optimal Peak Capture          
T-05  SHORT 95.0   94.5        +47.00   650.00      7.2%      20.0% Premature Run-Up Exit         

Forensic insights from the audit ledger

The audit report illuminates critical operational dynamics obscured by gross dollar accounting:

  1. Trade T-01 vs. Trade T-02: Both trades entered at $100.00 on the same date and experienced the identical peak excursion of $18.00 per share ($1,800.00 gross MFE). Trade T-01 utilized a disciplined limit target to secure a 94.3% net exit efficiency (+4.24R net). In contrast, Trade T-02 relied on a loose trailing stop that surrendered over 55% of open profits, delivering an exit capture of only 44.3% (+1.99R net).
  2. The Disastrous Round-Trip in T-03: Trade T-03 reached a peak favorable run-up of +$1,350.00 (+2.08R) on May 3. However, with no profit-taking mechanism or break-even stop in place, the position sat through an 8-day reversal until it triggered a stop-out at $99.00 on May 10, delivering a net loss of -$553.00 (-0.85R). In MFE analysis, round-tripped trades represent severe strategy leakage that directly damages expectancy.
  3. Execution-Aware Filtering in T-05: Because Trade T-05 entered at CLOSE on May 11 at $95.00, the intrabar dip to $94.50 that occurred earlier during the May 11 session was properly excluded from post-entry MFE. Only post-fill bars contributed to favorable excursion.

The MFE vs Realized PnL scatter matrix: Diagnosing trade health

The primary analytical visualization in MFE trading research is the MFE vs. Realized PnL Scatter Matrix. By plotting every trade's peak favorable excursion on the horizontal x-axis against its final realized net return on the vertical y-axis, researchers expose the structural behavior of their exit rules.

Code
Realized PnL (R) ^  (Diagonal '/' = 100% Efficiency Frontier)
+5.0R |                                      //4|
+4.4R |                                  //1/   |
+3.8R |                             /////       |
+3.2R |                        /////            |
+2.7R |                   /////                 |
+2.1R |               ////                 2    |
+1.5R |          /////                          |
+0.9R |      ////                               |
+0.3R | /////                    5              |
-0.2R |/----------------------------------------|
-0.8R |                 3                       |
-1.4R |                                         |
-2.0R |                                         |
     +-----------------------------------------> MFE (R)
      0.0R                               5.0R

Interpreting the four trade quadrants

In an MFE scatter plot, positions distribute across distinct behavioral quadrants bounded by the 100% Efficiency Frontier (Realized PnL = MFE):

Quadrant / ZoneMFE RangeRealized PnLExit EfficiencyStructural Diagnosis
100% Frontier HuggersHigh (> 2.5R)High (> 2.0R)> 80%Disciplined limit profit-taking or tight trailing stops during clean trends. (Trades 1 & 4)
Decayed Trend RunnersHigh (> 2.5R)Moderate (1.0R - 2.0R)40% - 70%Standard trend-following behavior. Trailing stop gives back initial retracement to ride macro runs. (Trade 2)
Premature ExitsHigh (> 2.0R)Low (< 0.5R)< 25%Position exited prematurely on noise; market proceeded to run strongly in the intended direction. (Trade 5)
Surrendered Round-TrippersSignificant (> 1.5R)Negative (< 0.0R)NegativeSevere exit failure. Trade accumulated substantial open paper profit but crashed into full stop-loss. (Trade 3)

No trade can ever plot above the diagonal frontier (/), because terminal realized profit cannot mathematically exceed the maximum price run-up reached during the trade. The closer a trade sits to the diagonal line, the higher its exit efficiency.


Calibrating profit targets and trailing stops without curve-fitting

A pervasive hazard in algorithmic trading research is post-hoc peak optimization. A researcher plots the historical MFE distribution of winning trades, notices that the top 10% reached +4.0R, and sets a fixed take-profit limit at +4.0R.

This is a classic form of data snooping. Extreme MFE values follow Pareto-like fat-tailed distributions. Targeting the extreme right tail of in-sample MFE causes out-of-sample performance to collapse: trades reverse at +3.2R or +3.8R, fail to trigger the optimistic target, and subsequently reverse into stop-outs or scratches.

In-sample calibration vs. out-of-sample holdout validation

Rather than cherry-picking historical peaks, robust MFE engineering relies on:

  1. Quantile Benchmarking: Using conservative in-sample percentiles (such as the median 50th or 65th percentile of MFE) to set fixed profit targets.
  2. Dynamic Trailing Stops: Activating a trailing stop only after an empirical MFE threshold is breached, locking in open gains without capping extreme winners (see Backtrader Trailing Stop).
  3. Strict Holdout Testing: Calibrating candidate exit rules strictly on an In-Sample (IS) training split and verifying net expectancy on an untouched Out-of-Sample (OOS) holdout split.

The following Python script simulates an empirical holdout experiment comparing four distinct exit architectures across in-sample training and out-of-sample testing sets:

Code
import statistics
from dataclasses import dataclass
from typing import List, Optional, Tuple

@dataclass
class SyntheticTradeTrajectory:
    trade_id: str
    max_mfe_r: float
    final_reversal_r: float  # Realized return under baseline trend signal

def evaluate_target_policy(
    trades: List[SyntheticTradeTrajectory],
    target_r: Optional[float] = None,
    trailing_trigger: Optional[Tuple[float, float]] = None
) -> Tuple[float, float, float]:
    """
    Evaluates net realized R across trades under different exit policies:
    - Fixed Target: Exits at target_r if max_mfe_r >= target_r; else exits at base signal.
    - Adaptive Trailing Stop: If max_mfe_r >= trigger_r, trails peak by trail_distance_r.
    - Unmanaged Base Signal: Exits at final_reversal_r.
    """
    pnl_r_list = []
    for t in trades:
        if target_r is not None:
            if t.max_mfe_r >= target_r:
                pnl = target_r
            else:
                pnl = t.final_reversal_r
        elif trailing_trigger is not None:
            trigger, trail_dist = trailing_trigger
            if t.max_mfe_r >= trigger:
                pnl = max(t.final_reversal_r, t.max_mfe_r - trail_dist)
            else:
                pnl = t.final_reversal_r
        else:
            pnl = t.final_reversal_r
        pnl_r_list.append(pnl)

    total_r = sum(pnl_r_list)
    win_rate = (len([p for p in pnl_r_list if p > 0]) / len(pnl_r_list)) * 100.0
    expectancy_r = total_r / len(pnl_r_list)
    return total_r, win_rate, expectancy_r

# In-sample trades (20 trials)
is_trades = [
    SyntheticTradeTrajectory(f"IS-{i+1:02d}", mfe, fin) for i, (mfe, fin) in enumerate([
        (3.8, 1.2), (1.5, -1.0), (4.2, 3.5), (0.8, -1.0), (2.4, 0.5),
        (3.1, -0.5), (1.9, 1.8), (0.4, -1.0), (2.8, 2.2), (3.6, 1.0),
        (1.2, -1.0), (4.5, 4.0), (2.1, -0.2), (0.6, -1.0), (3.3, 2.0),
        (1.7, 1.5), (2.9, 0.8), (0.5, -1.0), (3.9, 2.5), (2.2, -0.8)
    ])
]

# Out-of-sample holdout trades (20 trials)
oos_trades = [
    SyntheticTradeTrajectory(f"OOS-{i+1:02d}", mfe, fin) for i, (mfe, fin) in enumerate([
        (1.4, -1.0), (2.6, 1.1), (0.7, -1.0), (3.2, 0.4), (1.8, 1.6),
        (0.5, -1.0), (2.1, -0.4), (3.7, 2.8), (1.1, -1.0), (2.9, 1.5),
        (0.9, -1.0), (2.3, 0.2), (1.6, 1.2), (3.4, -0.5), (0.6, -1.0),
        (2.7, 2.1), (1.3, -1.0), (3.0, 0.9), (0.8, -1.0), (2.2, 1.8)
    ])
]

policies = {
    "Unmanaged Base Signal": (None, None),
    "Curve-Fitted Max MFE (4.0R Target)": (4.0, None),
    "Calibrated Median MFE (2.2R Target)": (2.2, None),
    "Adaptive Trailing Stop (Trig 1.8R, Trail 0.8R)": (None, (1.8, 0.8)),
}

print("=== IN-SAMPLE STRATEGY PERFORMANCE ===")
for name, (tgt, trail) in policies.items():
    tot, win, exp = evaluate_target_policy(is_trades, tgt, trail)
    print(f"{name:<45} | Total: {tot:+5.1f}R | WinRate: {win:4.1f}% | Expectancy: {exp:+5.2f}R")

print("\n=== OUT-OF-SAMPLE HOLDOUT PERFORMANCE ===")
for name, (tgt, trail) in policies.items():
    tot, win, exp = evaluate_target_policy(oos_trades, tgt, trail)
    print(f"{name:<45} | Total: {tot:+5.1f}R | WinRate: {win:4.1f}% | Expectancy: {exp:+5.2f}R")

Holdout validation experiment results

Code
=== IN-SAMPLE STRATEGY PERFORMANCE ===
Unmanaged Base Signal                         | Total: +13.5R | WinRate: 55.0% | Expectancy: +0.68R
Curve-Fitted Max MFE (4.0R Target)            | Total: +14.0R | WinRate: 55.0% | Expectancy: +0.70R
Calibrated Median MFE (2.2R Target)           | Total: +21.3R | WinRate: 65.0% | Expectancy: +1.06R
Adaptive Trailing Stop (Trig 1.8R, Trail 0.8R) | Total: +27.1R | WinRate: 70.0% | Expectancy: +1.35R

=== OUT-OF-SAMPLE HOLDOUT PERFORMANCE ===
Unmanaged Base Signal                         | Total:  +4.7R | WinRate: 50.0% | Expectancy: +0.24R
Curve-Fitted Max MFE (4.0R Target)            | Total:  +4.7R | WinRate: 50.0% | Expectancy: +0.24R
Calibrated Median MFE (2.2R Target)           | Total: +14.2R | WinRate: 55.0% | Expectancy: +0.71R
Adaptive Trailing Stop (Trig 1.8R, Trail 0.8R) | Total: +15.5R | WinRate: 60.0% | Expectancy: +0.78R

Analysis of the validation results

The holdout test reveals a textbook example of backtest overfitting:

  • The Failure of the Curve-Fitted 4.0R Target: In-sample, the 4.0R target slightly improved expectancy (+0.70R vs +0.68R). However, on the out-of-sample holdout dataset, the 4.0R target performed identically to the unmanaged baseline (+0.24R expectancy). Not a single out-of-sample trade reached 4.0R! As a result, the rule provided zero risk protection, demonstrating how post-hoc peak hunting fails out-of-sample.
  • The Superiority of Median Calibration and Adaptive Trailing: The calibrated median target (+2.2R) and the adaptive trailing stop preserved strong out-of-sample generalization. By triggering once a trade reached +1.8R and trailing at 0.8R distance, the adaptive trailing rule protected open profits from round-tripping while allowing runners to continue, tripling out-of-sample expectancy from +0.24R to +0.78R.

For rigorous strategies, combining MFE calibration with Walk-Forward Optimization prevents over-optimization and protects against regime shifts.


Division of labor: MAE vs MFE in systematic strategy design

Quantitative researchers frequently ask how to partition analytical responsibilities between Maximum Adverse Excursion and Maximum Favorable Excursion. Both metrics examine intermediate price excursions, but they serve complementary, non-overlapping roles in trading architecture:

Engineering DimensionMaximum Adverse Excursion (MAE)Maximum Favorable Excursion (MFE)
Primary FocusDefensive risk exposure and paper drawdownOffensive profit potential and run-up capture
Key Question Answered"How much adverse heat did this trade endure before reaching profit?""How much paper profit did this trade create before liquidation?"
System Component AuditedInitial stop-loss placement and entry timing precisionTake-profit boundaries, trailing stops, and profit harvesting
Pathology DetectedLucky survivor winners that nearly wiped out capitalRound-trip losses that surrendered substantial open gains
Optimization HazardSetting stops too tight, inducing whipsaw liquidationsCherry-picking peak MFE targets, causing out-of-sample failures
Cross-Referenced MetricInitial Risk Multiple (R-units) & Backtrader Stop LossExit Efficiency (%) & Backtrader Trailing Stop
Code
flowchart TD
    subgraph StrategyArchitecture ["Systematic Strategy Design"]
        EntryRule["Signal Entry Rules"]
        StopRule["Defensive Stop Rules (Calibrated via MAE)"]
        ExitRule["Harvesting Exit Rules (Calibrated via MFE)"]
    end

    EntryRule -->|"Excursion Auditing"| StopRule
    EntryRule -->|"Harvest Auditing"| ExitRule

    StopRule -.->|"Eliminates Catastrophes"| RobustPerformance["Robust Capital Preservation"]
    ExitRule -.->|"Captures Edge"| RobustPerformance

By decoupling risk management (MAE) from profit harvesting (MFE), quantitative developers avoid confusing trade quality with luck. A system where winning trades routinely absorb 0.9R of adverse heat is fragile, regardless of its win rate. Similarly, a system where average exit efficiency is under 20% is squandering alpha through sloppy execution.


Strategy audit checklist & OpenBacktesting architecture

Before deploying an algorithmic model or finalizing parameter sets, verify your trade exit architecture against this systematic audit checklist:

Verification DimensionAudit QuestionTarget BenchmarkCorrective Action
Average Run-Up CaptureWhat percentage of MFE is retained as net realized PnL across winners?> 50% exit efficiencyIf exit capture is < 35%, implement an adaptive trailing stop or tighter target.
Round-Trip FrequencyWhat percentage of trades with MFE > 1.5R terminate in a net loss?0% of tradesIntroduce a break-even stop rule triggered once MFE crosses +1.0R to +1.5R.
Intrabar Timing AccuracyAre CLOSE entries isolated from pre-fill highs and lows?0.0 phantom MFE on entry barEnforce execution-aware bar slicing; do not naively include entire entry bar.
Transaction Drag InclusionDoes exit efficiency account for round-turn commission and slippage?Net efficiency > 0%Subtract realistic fees; discard targets where friction consumes > 30% of profit.
Holdout GeneralizationAre profit targets validated on untouched out-of-sample data?OOS expectancy degradation < 30%Reject any exit target set to historical maximum MFE quantiles.

How OpenBacktesting enables inspectable trade exit audits

OpenBacktesting is an independent Python backtesting engine (0.2.1), built for Python >=3.12,&lt;3.15, architected specifically for single-asset historical OHLCV bar-by-bar research.

Standard backtesting tools often aggregate trade results into high-level summary dictionaries, discarding the granular price path traversed between entry and exit. OpenBacktesting enforces inspectability at every stage of the research lifecycle:

  • Bar-by-Bar Causal Execution: Orders are processed sequentially against incoming OHLCV bars without lookahead leakage. Fills and excursion limits are recorded with exact timestamp precision.
  • SQLite Trial Persistence: Every trial execution automatically stores full trade lifecycles, atomic child orders, and equity curve histories directly into SQLite. This allows quantitative researchers to run automated SQL or Python queries auditing trade-level MAE and MFE across thousands of parameter trials.
  • Local Studio UI: OpenBacktesting provides an inspectable local Studio interface to inspect individual trade trajectories, entry/exit markers, and maximum adverse excursion extremes bar by bar.

By auditing the full price path of every position rather than blindly accepting realized PnL, algorithmic researchers can identify genuine edge, reject overfitted parameters, and build robust systematic trading strategies grounded in empirical market realities.