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:
- 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).
- 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.
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"| ExitEffTo 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].
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:
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)) * 100Where 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:
Run-Up Capture Efficiency (%) = (Realized Net PnL_$ / Gross MFE_$) * 100Where:
Realized Net PnL_$accounts for round-turn trading commissions, exchange fees, and bid-ask slippage.Gross MFE_$represents the peak theoretical dollar profit reachable atP_max(longs) orP_min(shorts).
Transaction Cost Drag:
Realized Net PnL_$ = Realized Gross PnL_$ - (2 * Commission_Per_Share + 2 * Slippage_Per_Share) * SizeWhen 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.
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
CLOSEfills, the entry bar's contribution to post-entry excursion is strictly bounded at zero. Effective price tracking for MFE must begin atP_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.
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:
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:
- 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).
- 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.
- Execution-Aware Filtering in T-05: Because Trade T-05 entered at
CLOSEon 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.
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.0RInterpreting 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 / Zone | MFE Range | Realized PnL | Exit Efficiency | Structural Diagnosis |
|---|---|---|---|---|
| 100% Frontier Huggers | High (> 2.5R) | High (> 2.0R) | > 80% | Disciplined limit profit-taking or tight trailing stops during clean trends. (Trades 1 & 4) |
| Decayed Trend Runners | High (> 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 Exits | High (> 2.0R) | Low (< 0.5R) | < 25% | Position exited prematurely on noise; market proceeded to run strongly in the intended direction. (Trade 5) |
| Surrendered Round-Trippers | Significant (> 1.5R) | Negative (< 0.0R) | Negative | Severe 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:
- Quantile Benchmarking: Using conservative in-sample percentiles (such as the median 50th or 65th percentile of MFE) to set fixed profit targets.
- 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).
- 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:
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
=== 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.78RAnalysis 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 Dimension | Maximum Adverse Excursion (MAE) | Maximum Favorable Excursion (MFE) |
|---|---|---|
| Primary Focus | Defensive risk exposure and paper drawdown | Offensive 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 Audited | Initial stop-loss placement and entry timing precision | Take-profit boundaries, trailing stops, and profit harvesting |
| Pathology Detected | Lucky survivor winners that nearly wiped out capital | Round-trip losses that surrendered substantial open gains |
| Optimization Hazard | Setting stops too tight, inducing whipsaw liquidations | Cherry-picking peak MFE targets, causing out-of-sample failures |
| Cross-Referenced Metric | Initial Risk Multiple (R-units) & Backtrader Stop Loss | Exit Efficiency (%) & Backtrader Trailing Stop |
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"| RobustPerformanceBy 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 Dimension | Audit Question | Target Benchmark | Corrective Action |
|---|---|---|---|
| Average Run-Up Capture | What percentage of MFE is retained as net realized PnL across winners? | > 50% exit efficiency | If exit capture is < 35%, implement an adaptive trailing stop or tighter target. |
| Round-Trip Frequency | What percentage of trades with MFE > 1.5R terminate in a net loss? | 0% of trades | Introduce a break-even stop rule triggered once MFE crosses +1.0R to +1.5R. |
| Intrabar Timing Accuracy | Are CLOSE entries isolated from pre-fill highs and lows? | 0.0 phantom MFE on entry bar | Enforce execution-aware bar slicing; do not naively include entire entry bar. |
| Transaction Drag Inclusion | Does exit efficiency account for round-turn commission and slippage? | Net efficiency > 0% | Subtract realistic fees; discard targets where friction consumes > 30% of profit. |
| Holdout Generalization | Are 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,<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.
