Beyond realized PnL: The path-dependency of trade risk
In quantitative strategy research and algorithmic backtesting, researchers routinely commit a fundamental attribution error: evaluating trade quality solely by terminal realized Profit and Loss (PnL). A trade that enters at $100.00 and exits at $115.00 is logged in the trade ledger as a profitable +$1,500 win (assuming 100 shares). However, terminal accounting conceals the path the price traversed between fill and exit.
Consider two hypothetical long trades executed with identical capital allocation and identical realized profits:
- Trade Alpha: Enters at $100.00, experiences an immediate adverse dip to $98.80 (a temporary drawdown of $1.20 per share, or 1.2%), then trends smoothly upward to close at $115.00.
- Trade Beta: Enters at $100.00, plummets to $82.50 (an intraday paper loss of $17.50 per share, or 17.5%), hovers on the brink of catastrophic liquidation for twelve trading sessions, and miraculously rebounds on an earnings headline to exit at $115.00.
On a standard broker equity statement, Trade Alpha and Trade Beta appear identical. In reality, Trade Beta was an uncontrolled risk disaster disguised as a winning trade by survivor bias. Had a conventional risk management rule or capital preservation stop been active, Trade Beta would have been liquidated at a severe loss.
flowchart TD
subgraph ExecutionPath ["Trade Lifecycle Price Path"]
Entry["Entry Fill (P_entry)"]
Adverse["Maximum Adverse Excursion (Peak Paper Loss)"]
Favorable["Maximum Favorable Excursion (Peak Paper Gain)"]
Exit["Exit Fill (P_exit)"]
end
Entry -->|"Price Trajectory"| Adverse
Adverse -->|"Recovery / Trend"| Favorable
Favorable -->|"Terminal Liquidation"| Exit
subgraph AuditMetrics ["Forensic Trade Risk Metrics"]
MAE["MAE: Worst Intrabar Price Deficit"]
MFE["MFE: Best Intrabar Price Surplus"]
EOT["Realized PnL: Terminal State Only"]
end
Adverse -.->|"Quantifies Heat"| MAE
Favorable -.->|"Quantifies Potential"| MFE
Exit -.->|"Quantifies Outcome"| EOTTo quantify the intermediate price distress of open positions, John Sweeney introduced the concept of Maximum Adverse Excursion (MAE) in his 1992 treatise Campaign Trading. In quantitative finance and algorithmic engineering, MAE measures the maximum unrealized loss (peak adverse paper drawdown) experienced by a position throughout its active holding duration.
Evaluating MAE across historical trades serves three critical engineering objectives:
- Auditing Entry Timing Efficiency: Strategies with genuine alpha enter positions near local inflections, exhibiting minimal adverse excursion before moving into profit.
- Quantifying Structural Edge: By mapping realized returns against adverse excursion, researchers separate repeatable systematic alpha from lucky recoveries.
- Informing Stop Loss Architecture: Rather than guessing arbitrary stop boundaries, MAE provides empirical distributions of how much room winning trades actually require.
Mathematical formulation of MAE across long and short positions
MAE must be computed rigorously across both long and short trade directions. Depending on portfolio construction, sizing models, and volatility regimes, MAE should be expressed in three distinct units: absolute dollar loss, percentage drawdown from entry, and normalized initial risk multiples (R-units).
Long position formulations
For a long trade entering at price P_entry with position size Q at timestamp t_entry and closing at t_exit, the market generates a sequence of historical bars with low prices L_t during the holding interval t \in [t_entry, t_exit].
The lowest price reached during the holding lifecycle is:
Min_Low = min(L_t) for t in [t_entry, t_exit]The adverse price distance for a long trade is:
Adverse_Distance_Long = max(0.0, P_entry - Min_Low)From this distance, three complementary metrics are derived:
Dollar_MAE_Long = Adverse_Distance_Long * Q
Percent_MAE_Long = (Adverse_Distance_Long / P_entry) * 100
MAE_R_Long = Adverse_Distance_Long / (P_entry - P_stop)Where P_stop represents the predefined initial nominal stop-loss price, defining 1R = P_entry - P_stop. An MAE of 0.40R indicates that the trade traversed only 40% of its allotted risk budget before recovering or reaching its target.
Short position formulations
Short trades invert the directional mechanics. For a short position entering at P_entry with quantity Q, risk increases when the market rallies above the entry price. Let H_t represent the high prices recorded during the trade's holding window:
Max_High = max(H_t) for t in [t_entry, t_exit]The adverse price distance for a short trade is:
Adverse_Distance_Short = max(0.0, Max_High - P_entry)The resulting metrics are:
Dollar_MAE_Short = Adverse_Distance_Short * Q
Percent_MAE_Short = (Adverse_Distance_Short / P_entry) * 100
MAE_R_Short = Adverse_Distance_Short / (P_stop - P_entry)Where 1R = P_stop - P_entry for P_stop > P_entry.
flowchart LR
subgraph LongDynamics ["Long Trade Dynamics"]
L_Entry["P_entry"]
L_Low["Min Low (t in holding)"]
L_Stop["P_stop (Initial Risk)"]
L_Dist["Adverse Distance = P_entry - Min Low"]
L_Entry --- L_Dist --> L_Low
L_Entry -.->|"1.0 R Budget"| L_Stop
end
subgraph ShortDynamics ["Short Trade Dynamics"]
S_Stop["P_stop (Initial Risk)"]
S_High["Max High (t in holding)"]
S_Entry["P_entry"]
S_Dist["Adverse Distance = Max High - P_entry"]
S_High <-- S_Dist --- S_Entry
S_Stop <.-|"1.0 R Budget"| S_Entry
endWhy normalization in R-units is mandatory
Evaluating adverse excursion purely in dollars or percentage terms introduces severe distortion when comparing trades across different asset classes, price regimes, or dynamic position sizing algorithms (such as Average True Range volatility sizing).
For example, a $5.00 adverse move on a $500 stock represents a 1.0% adverse excursion. On a $25 stock, a $5.00 adverse move represents a 20.0% catastrophe. Normalizing adverse excursion by the initial planned risk unit R ensures scale independence:
| Metric Scale | Calculation Method | Primary Advantage | Analytical Blind Spot |
|---|---|---|---|
| Dollar MAE ($) | Adverse_Distance * Q | Direct monetary impact on portfolio cash. | Distorted by position sizing variations and asset price level. |
| Percentage MAE (%) | (Adverse_Distance / P_entry) * 100 | Standardized cross-asset price volatility. | Ignores leverage, nominal account risk, and volatility-adjusted sizing. |
| Initial Risk (R-Units) | Adverse_Distance / (P_entry - P_stop) | Pure risk-budget consumption metric; comparable across any strategy. | Requires an explicit initial stop loss parameter at order inception. |
| Holding Duration (Bars) | t_exit - t_entry | Exposes time decay and capital tie-up. | Contains no price or monetary magnitude information. |
Intrabar path ambiguity: The entry and exit bar trap
When performing forensic trade audits on historical bar data (such as 1-minute, 1-hour, or 1-day OHLCV bars), quantitative developers encounter the intrabar path ambiguity problem.
A single bar records four summary statistics: Open, High, Low, and Close. It does not record the microsecond price trajectory between those points. On intermediate bars where a position is held continuously, this ambiguity is harmless: the bar's Low was experienced while the position was active, regardless of whether the high or low came first.
On the entry bar and exit bar, however, naive calculations produce severe errors:
flowchart TD
subgraph BarAmbiguity ["The Entry Bar Ordering Dilemma"]
Open["Bar Open: $100.00"]
High["Bar High: $105.00"]
Low["Bar Low: $92.00"]
Close["Bar Close: $104.00"]
end
subgraph ScenarioA ["Scenario A: Low Before Entry"]
A1["Market Opens at $100.00"] --> A2["Morning Flush to Low: $92.00"]
A2 --> A3["Afternoon Entry Buy Fill at $99.00"]
A3 --> A4["Close at $104.00"]
A_Result["True Post-Entry Adverse Excursion = $0.00\n(Naive Code Reports $7.00 Loss!)"]
end
subgraph ScenarioB ["Scenario B: Low After Entry"]
B1["Market Opens at $100.00"] --> B2["Morning Entry Buy Fill at $100.00"]
B2 --> B3["Midday Plunge to Low: $92.00"]
B3 --> B4["Close at $104.00"]
B_Result["True Post-Entry Adverse Excursion = $8.00\n(Legitimate Position Heat)"]
endThe three execution timing conventions
To avoid polluting your MAE audit with phantom drawdowns, your ledger parser must enforce explicit execution timing policies:
- Market-on-Open Fills (
OPEN): If the order executed at the bar'sOpen, the entire subsequent price path of that bar (High,Low,Close) occurred after position establishment. The entry bar's extreme is fully valid for MAE calculation. - Market-on-Close Fills (
CLOSE): If the order executed at the bar'sClose, the precedingOpen,High, andLowoccurred before the trade existed. For a long trade, any dip to the bar'sLowduring the session was not suffered by the portfolio. Including that low would artificially inflate MAE. The entry bar adverse distance is zero. - Intrabar Limit and Stop Fills (
LIMIT/STOP): If an order fills intrabar at a specific limit priceP_limit, only price action penetrating beyondP_limitin the adverse direction can be attributed to the trade.
Failing to account for entry bar boundaries creates phantom MAE spikes, causing researchers to overestimate strategy risk or place unnecessarily loose stop losses.
The Sweeney MAE scatter plot: Deconstructing trade efficiency
The core analytical tool of MAE trading analysis is the MAE scatter plot. By plotting Maximum Adverse Excursion on the horizontal X-axis against Realized PnL (or MFE) on the vertical Y-axis, an algorithmic strategy's execution edge becomes visually inspectable.
Realized PnL (R-Multiples)
+
+4R | * (T-101: Clean Winner)
| *
+2R | * *
| * * (T-104: Survivor Winner)
+1R | *
|
- - - - - - - - - - 0 - - - - - - - - - - - - - - - - - - - - -> MAE (R-Units)
| 0.5R 1.0R 1.5R
-1R | * (T-103: Cut) * (T-105: Gap Loss)
|
-2R |
+The four quadrants of trade execution
Analyzing the distribution of trades across the MAE coordinate space reveals the underlying mechanics of your trading rules:
- Quadrant I (High Profit, Low MAE) — "The Edge Cluster":
Trades that generate substantial positive returns while enduring negligible adverse excursion (e.g.,
MAE < 0.35R). These represent high-conviction entries where the market immediately validates the directional premise. - Quadrant II (High Profit, High MAE) — "The Survivor Cluster":
Trades that finished with strong gains but suffered severe intermediate paper losses (e.g.,
MAE > 0.80R). These trades survived solely because no protective stop intervened. They represent negative alpha that was bailed out by random market volatility or macro drift. - Quadrant III (Loss, Low MAE) — "Disciplined Cutoffs": Trades that were closed quickly upon invalidation, suffering minimal adverse damage before capital was reclaimed.
- Quadrant IV (Loss, High MAE) — "Runaway Tail Losses":
Trades that absorbed maximum heat and exited at severe losses. If trades cluster beyond
1.0R, the execution engine suffers from stop-loss execution slippage, illiquidity gaps, or delayed manual interventions.
The post-hoc fallacy: Why naive MAE cutoff stops fail in production
The most prevalent error in adverse excursion research is the post-hoc MAE cutoff fallacy.
A researcher plots the MAE distribution for 500 historical backtest trades. They observe that 92% of all winning trades never experienced an adverse excursion greater than 2.0%. Meanwhile, 70% of losing trades suffered adverse excursions exceeding 3.5%.
The researcher concludes:
"If I simply introduce a rigid 2.1% stop loss, I will eliminate all large losing trades while leaving 92% of my winners completely untouched! My profit factor will double!"
flowchart TD
subgraph FlawedLogic ["The Post-Hoc Stop Loss Trap"]
Obs["Observation: 92% of historical winners had MAE < 2.0%"]
Naive["Faulty Inference: Place stop loss at 2.1%"]
Sim["Rerun Simulation with New Stop"]
Failure["Catastrophic Drop in Expectancy & Win Rate"]
end
Obs --> Naive --> Sim --> Failure
subgraph TrueCausality ["Microstructural Reality"]
Path["Price paths are stochastic and path-dependent"]
Interfere["Tighter stop alters execution lifecycle"]
Whipsaw["Market noise hits 2.1% stop before trend emerges"]
Conversion["Former 92% winners become immediate realized losses"]
end
Naive -.-> Interfere
Interfere --> Path --> Whipsaw --> ConversionWhy retrofitting stops destroys expectancy
This naive reasoning fails in live execution and rigorous walk-forward backtests due to three mathematical realities:
- Path Interference and Trade Mutation: A historical trade that had an MAE of 1.95% did not touch 1.95% and freeze. It fluctuated within a noisy distribution. Introducing a stop at 2.1% increases the conditional probability that market microstructure noise (bid-ask bounce, liquidity sweeps, spread widening) will prematurely trigger the stop, transforming a multi-R winning trade into an immediate realized loss.
- Survivor Bias of Historical Unhedged Positions: In an unconstrained backtest, trades that experienced deep excursions had infinite time to recover. Once you introduce a mechanical stop, you alter the state space of the system.
- Parameter Overfitting: Optimizing a stop loss directly on in-sample MAE quantiles is the definition of data snooping. The exact point where historical winners clustered is an in-sample artifact of that specific historical price trajectory.
To implement mechanical stops safely without falling into the post-hoc trap, review our in-depth engineering guide on Backtrader Stop Loss to understand order execution timing, gap opening slippage, and orphan order cleanup.
Order ledger reconciliation: Converting fills to trade lifecycles
To calculate MAE, raw broker execution orders must be parsed into matched, flat-to-flat Trade Lifecycles.
A standard backtest broker emits atomic order fill events:
- Order #1:
BUY 100 AAPL @ 150.00(Opens position) - Order #2:
BUY 50 AAPL @ 152.00(Scales in) - Order #3:
SELL 150 AAPL @ 158.00(Closes position flat)
To audit adverse excursion:
- Parse the order fills using First-In-First-Out (FIFO) or Volume-Weighted Average Price (VWAP) accounting.
- Determine the exact start timestamp (
t_entry) and flat timestamp (t_exit). - Slice the underlying OHLCV bar database strictly between
[t_entry, t_exit]. - Apply the execution-aware boundary rules to avoid entry/exit bar bias.
- Compute the adverse and favorable excursion extrema.
sequenceDiagram
autonumber
participant Engine as Backtest Engine / Ledger
participant Parser as Order Reconciler
participant DB as Historical OHLCV Store
participant Auditor as MAE Risk Auditor
Engine->>Parser: Stream Order Fills (Entry, Adjustments, Exit)
Parser->>Parser: Reconcile Fills into Flat-to-Flat Trade Lifecycle
Parser->>DB: Query Bars between [t_entry, t_exit]
DB-->>Auditor: Return OHLCV Slice
Auditor->>Auditor: Resolve Intrabar Ambiguity on Entry/Exit Bars
Auditor->>Auditor: Calculate MAE ($ / % / R) and MFE
Auditor-->>Engine: Append Audit Metrics to Trial Trade RecordEnd-to-end Python implementation: Auditing an order and trade ledger
Below is a complete, self-contained Python implementation designed to audit trade risk, calculate MAE and MFE across long and short positions, and categorize trade execution quality.
It uses modern Python standard library structures (dataclasses, typing) with zero external dependencies, making it immediately embeddable into any quantitative pipeline.
from dataclasses import dataclass
from typing import List, Literal, Optional
@dataclass(frozen=True)
class OHLCVBar:
"""Historical bar representation for backtest reconciliation."""
timestamp: str
open: float
high: float
low: float
close: float
volume: float
@dataclass(frozen=True)
class TradeRecord:
"""Flat-to-flat trade record derived from order fills."""
trade_id: str
symbol: 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"] = "CLOSE"
initial_stop: Optional[float] = None
@dataclass(frozen=True)
class AuditResult:
"""Forensic risk audit output for an individual trade lifecycle."""
trade_id: str
direction: str
entry_price: float
exit_price: float
realized_pnl: float
realized_pct: float
realized_r: Optional[float]
mae_dollar: float
mae_pct: float
mae_r: Optional[float]
mfe_dollar: float
mfe_pct: float
mfe_r: Optional[float]
holding_bars: int
classification: str
def audit_trade_lifecycle(
trade: TradeRecord,
bars: List[OHLCVBar],
intrabar_policy: str = "execution_aware"
) -> AuditResult:
"""
Audit an individual trade lifecycle against underlying OHLCV bars.
Handles directional differences (LONG vs SHORT) and resolves intrabar
path ambiguity on entry/exit bars based on execution timing.
"""
# Slice bars within the trade's active window
trade_bars = [b for b in bars if trade.entry_time <= b.timestamp <= trade.exit_time]
if not trade_bars:
raise ValueError(f"Zero bars found for trade {trade.trade_id} between {trade.entry_time} and {trade.exit_time}")
n_bars = len(trade_bars)
# Resolve intrabar adverse extremes with path awareness
if trade.direction == "LONG":
realized_pnl = (trade.exit_price - trade.entry_price) * trade.size
realized_pct = ((trade.exit_price - trade.entry_price) / trade.entry_price) * 100.0
# Calculate lowest low and highest high
if intrabar_policy == "execution_aware" and trade.entry_exec == "CLOSE":
# If entered at close of entry bar, entry bar's low occurred prior to fill
effective_lows = [trade.entry_price] + [b.low for b in trade_bars[1:]]
effective_highs = [trade.entry_price] + [b.high for b in trade_bars[1:]]
else:
effective_lows = [b.low for b in trade_bars]
effective_highs = [b.high for b in trade_bars]
min_low = min(effective_lows)
max_high = max(effective_highs)
# Long adverse excursion: dip below entry
mae_dist = max(0.0, trade.entry_price - min_low)
mae_dollar = mae_dist * trade.size
mae_pct = (mae_dist / trade.entry_price) * 100.0
# Long favorable excursion: rally above entry
mfe_dist = max(0.0, max_high - trade.entry_price)
mfe_dollar = mfe_dist * trade.size
mfe_pct = (mfe_dist / trade.entry_price) * 100.0
# Risk-unit (R) calculations
if trade.initial_stop is not None and trade.entry_price > trade.initial_stop:
r_budget = trade.entry_price - trade.initial_stop
mae_r = mae_dist / r_budget
mfe_r = mfe_dist / r_budget
realized_r = (trade.exit_price - trade.entry_price) / r_budget
else:
mae_r, mfe_r, realized_r = None, None, None
else: # SHORT
realized_pnl = (trade.entry_price - trade.exit_price) * trade.size
realized_pct = ((trade.entry_price - trade.exit_price) / trade.entry_price) * 100.0
if intrabar_policy == "execution_aware" and trade.entry_exec == "CLOSE":
effective_lows = [trade.entry_price] + [b.low for b in trade_bars[1:]]
effective_highs = [trade.entry_price] + [b.high for b in trade_bars[1:]]
else:
effective_lows = [b.low for b in trade_bars]
effective_highs = [b.high for b in trade_bars]
min_low = min(effective_lows)
max_high = max(effective_highs)
# Short adverse excursion: spike above entry
mae_dist = max(0.0, max_high - trade.entry_price)
mae_dollar = mae_dist * trade.size
mae_pct = (mae_dist / trade.entry_price) * 100.0
# Short favorable excursion: decline below entry
mfe_dist = max(0.0, trade.entry_price - min_low)
mfe_dollar = mfe_dist * trade.size
mfe_pct = (mfe_dist / trade.entry_price) * 100.0
if trade.initial_stop is not None and trade.initial_stop > trade.entry_price:
r_budget = trade.initial_stop - trade.entry_price
mae_r = mae_dist / r_budget
mfe_r = mfe_dist / r_budget
realized_r = (trade.entry_price - trade.exit_price) / r_budget
else:
mae_r, mfe_r, realized_r = None, None, None
# Classify execution quality
if realized_pnl > 0:
if mae_r is not None and mae_r < 0.35:
classification = "Efficient Alpha Winner"
elif mae_r is not None and mae_r >= 0.75:
classification = "High-MAE Survivor Winner"
else:
classification = "Standard Winner"
else:
if mae_r is not None and mae_r <= 1.05:
classification = "Disciplined Truncated Loss"
else:
classification = "Excess Tail Loss (Gap/Slippage)"
return AuditResult(
trade_id=trade.trade_id,
direction=trade.direction,
entry_price=trade.entry_price,
exit_price=trade.exit_price,
realized_pnl=round(realized_pnl, 2),
realized_pct=round(realized_pct, 2),
realized_r=round(realized_r, 2) if realized_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,
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,
holding_bars=n_bars,
classification=classification,
)
# Demonstration dataset and audit execution
if __name__ == "__main__":
historical_bars = [
OHLCVBar("2026-05-01", 100.0, 102.5, 98.8, 101.5, 1200000),
OHLCVBar("2026-05-02", 101.5, 106.0, 100.8, 105.2, 1450000),
OHLCVBar("2026-05-03", 105.0, 109.5, 104.2, 108.7, 1600000),
OHLCVBar("2026-05-04", 108.5, 115.5, 108.0, 115.0, 2100000),
OHLCVBar("2026-05-05", 114.5, 116.0, 111.0, 112.0, 1800000),
OHLCVBar("2026-05-06", 111.5, 113.0, 106.2, 107.0, 1300000),
OHLCVBar("2026-05-07", 106.8, 107.5, 103.5, 104.0, 1100000),
OHLCVBar("2026-05-08", 104.0, 108.5, 96.5, 107.5, 1950000),
OHLCVBar("2026-05-09", 107.5, 114.0, 106.0, 113.5, 1750000),
OHLCVBar("2026-05-10", 113.0, 113.5, 103.8, 104.0, 1650000),
OHLCVBar("2026-05-11", 104.0, 105.0, 99.0, 100.0, 1500000),
OHLCVBar("2026-05-12", 100.0, 101.5, 93.0, 94.0, 2200000),
OHLCVBar("2026-05-13", 94.5, 102.5, 94.0, 101.5, 2500000),
]
sample_trades = [
# T-101: Clean long breakout winner (low MAE)
TradeRecord("T-101", "EQTY", "LONG", "2026-05-01", 100.0, "2026-05-04", 115.0, 100, "OPEN", "CLOSE", initial_stop=96.0),
# T-102: High-MAE survivor winner (tested stop support at 96.5 before rally)
TradeRecord("T-102", "EQTY", "LONG", "2026-05-07", 104.0, "2026-05-09", 113.5, 100, "OPEN", "CLOSE", initial_stop=96.0),
# T-103: Disciplined truncated loss (stop hit on 2026-05-10)
TradeRecord("T-103", "EQTY", "LONG", "2026-05-09", 107.5, "2026-05-10", 104.0, 100, "OPEN", "STOP", initial_stop=104.0),
# T-104: Short trend winner (entered at close on 2026-05-10, clean downtrend)
TradeRecord("T-104", "EQTY", "SHORT", "2026-05-10", 104.0, "2026-05-12", 94.0, 100, "CLOSE", "CLOSE", initial_stop=107.0),
# T-105: Overnight gap loss (stop bypassed by adverse morning gap on 2026-05-13)
TradeRecord("T-105", "EQTY", "SHORT", "2026-05-12", 94.0, "2026-05-13", 102.5, 100, "CLOSE", "STOP", initial_stop=98.0),
]
print(f"{'ID':<6} {'Dir':<6} {'Entry':<6} {'Exit':<6} {'PnL ($)':<9} {'PnL (R)':<8} {'MAE ($)':<8} {'MAE (R)':<8} {'MFE (R)':<8} {'Classification':<32}")
print("-" * 105)
for t in sample_trades:
audit = audit_trade_lifecycle(t, historical_bars)
pnl_r_str = f"{audit.realized_r:+.2f}R" if audit.realized_r is not None else "N/A"
mae_r_str = f"{audit.mae_r:.2f}R" if audit.mae_r is not None else "N/A"
mfe_r_str = f"{audit.mfe_r:.2f}R" if audit.mfe_r is not None else "N/A"
print(f"{audit.trade_id:<6} {audit.direction:<6} {audit.entry_price:<6.1f} {audit.exit_price:<6.1f} {audit.realized_pnl:+8.2f} {pnl_r_str:<8} {audit.mae_dollar:7.2f} {mae_r_str:<8} {mfe_r_str:<8} {audit.classification:<32}")Script audit execution output
Executing the audit script across these trade records produces the following reconciliation report:
ID Dir Entry Exit PnL ($) PnL (R) MAE ($) MAE (R) MFE (R) Classification
---------------------------------------------------------------------------------------------------------
T-101 LONG 100.0 115.0 +1500.00 +3.75R 120.00 0.30R 3.88R Efficient Alpha Winner
T-102 LONG 104.0 113.5 +950.00 +1.19R 750.00 0.94R 1.25R High-MAE Survivor Winner
T-103 LONG 107.5 104.0 -350.00 -1.00R 370.00 1.06R 1.86R Disciplined Truncated Loss
T-104 SHORT 104.0 94.0 +1000.00 +3.33R 0.00 0.00R 3.67R Efficient Alpha Winner
T-105 SHORT 94.0 102.5 -850.00 -2.12R 850.00 2.12R 0.25R Excess Tail Loss (Gap/Slippage) Forensic insights from the ledger audit
The audit report immediately surfaces structural facts that a naive PnL report would omit:
- Trade T-101 vs Trade T-102:
Both trades generated substantial profits (+3.75R and +1.19R). However, T-101 only absorbed
0.30Rof adverse excursion, proving pristine entry timing. In contrast, T-102 absorbed0.94Rof heat, coming within $0.50 of its liquidation stop before rallying. A system reliant on T-102-style trades is fragile and prone to sudden drawdowns in choppy markets. - The Execution-Aware Advantage in T-104:
Notice Trade T-104: Because the parser registered the entry as
CLOSEon May 10, the morning high of $113.50 (which occurred prior to entry when the stock opened at $113.00 and dropped) was correctly excluded from the post-entry adverse excursion. A naive parser would have incorrectly penalized this short trade with a phantom $9.50 MAE. - Tail Risk Slippage in T-105:
Trade T-105 experienced an adverse gap on May 13. While the initial stop was pegged at $98.00 (a planned 1.0R loss of $400), the market gapped open and traded through $102.50, generating a
-2.12Rloss. Auditing MAE identifies where execution reality diverges from theoretical risk limits.
Strategy audit checklist & OpenBacktesting architecture
Before deploying an algorithmic strategy to live capital or incorporating historical backtest statistics into portfolio risk budgets, run through this systematic trade audit checklist:
| Verification Dimension | Audit Question | Target Benchmark | Corrective Action |
|---|---|---|---|
| Winner MAE Concentration | What percentage of winning trades experience an MAE < 0.50R? | > 80% of winning trades | If winners routinely absorb > 0.80R, refine entry filters or indicator lag. |
| Path Ambiguity Handling | Are entry bar extremes partitioned according to fill execution type? | Zero phantom MAE on CLOSE fills | Use execution-aware bar slicing; do not naively include entire entry bar. |
| Normalized Units | Is MAE audited in R-multiples alongside raw dollar drawdowns? | Scale-independent R-audit | Replace fixed dollar thresholds with volatility-adjusted (ATR) risk units. |
| Tail Loss Boundaries | Do losing trades strictly truncate at or near 1.0R? | Max loss ≤ 1.10R (accounting for slippage) | Investigate overnight gaps, liquidity depth, or stop execution delays. |
| Out-of-Sample Integrity | Was the stop loss parameter tested on untouched out-of-sample data? | OOS profit factor and expectancy stable | Reject any stop loss retrofitted directly to in-sample MAE quantiles. |
How OpenBacktesting enables inspectable trade risk 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.
Unlike black-box backtesting libraries that collapse simulation history into high-level summary dictionaries, OpenBacktesting maintains an immutable, inspectable event ledger:
- Bar-by-Bar Determinism: Orders are processed sequentially against incoming OHLCV bars, eliminating look-ahead leakage and enforcing strict timestamp ordering.
- 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.
