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:

  1. 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.
  2. 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.

Code
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"| EOT

To 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:

Code
Min_Low = min(L_t)  for t in [t_entry, t_exit]

The adverse price distance for a long trade is:

Code
Adverse_Distance_Long = max(0.0, P_entry - Min_Low)

From this distance, three complementary metrics are derived:

Code
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:

Code
Max_High = max(H_t)  for t in [t_entry, t_exit]

The adverse price distance for a short trade is:

Code
Adverse_Distance_Short = max(0.0, Max_High - P_entry)

The resulting metrics are:

Code
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.

Code
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
    end

Why 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 ScaleCalculation MethodPrimary AdvantageAnalytical Blind Spot
Dollar MAE ($)Adverse_Distance * QDirect monetary impact on portfolio cash.Distorted by position sizing variations and asset price level.
Percentage MAE (%)(Adverse_Distance / P_entry) * 100Standardized 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_entryExposes 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:

Code
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)"]
    end

The three execution timing conventions

To avoid polluting your MAE audit with phantom drawdowns, your ledger parser must enforce explicit execution timing policies:

  1. Market-on-Open Fills (OPEN): If the order executed at the bar's Open, 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.
  2. Market-on-Close Fills (CLOSE): If the order executed at the bar's Close, the preceding Open, High, and Low occurred before the trade existed. For a long trade, any dip to the bar's Low during the session was not suffered by the portfolio. Including that low would artificially inflate MAE. The entry bar adverse distance is zero.
  3. Intrabar Limit and Stop Fills (LIMIT / STOP): If an order fills intrabar at a specific limit price P_limit, only price action penetrating beyond P_limit in 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.

Code
                  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:

  1. Quadrant I (High Profit, Low MAE) — "The Edge Cluster": Trades that generate substantial positive returns while enduring negligible adverse excursion (e.g., MAE &lt; 0.35R). These represent high-conviction entries where the market immediately validates the directional premise.
  2. Quadrant II (High Profit, High MAE) — "The Survivor Cluster": Trades that finished with strong gains but suffered severe intermediate paper losses (e.g., MAE &gt; 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.
  3. Quadrant III (Loss, Low MAE) — "Disciplined Cutoffs": Trades that were closed quickly upon invalidation, suffering minimal adverse damage before capital was reclaimed.
  4. 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!"

Code
flowchart TD
    subgraph FlawedLogic ["The Post-Hoc Stop Loss Trap"]
        Obs["Observation: 92% of historical winners had MAE &lt; 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 --> Conversion

Why retrofitting stops destroys expectancy

This naive reasoning fails in live execution and rigorous walk-forward backtests due to three mathematical realities:

  1. 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.
  2. 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.
  3. 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:

  1. Parse the order fills using First-In-First-Out (FIFO) or Volume-Weighted Average Price (VWAP) accounting.
  2. Determine the exact start timestamp (t_entry) and flat timestamp (t_exit).
  3. Slice the underlying OHLCV bar database strictly between [t_entry, t_exit].
  4. Apply the execution-aware boundary rules to avoid entry/exit bar bias.
  5. Compute the adverse and favorable excursion extrema.
Code
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 Record

End-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.

mae_risk_auditor.py
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:

Code
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:

  1. Trade T-101 vs Trade T-102: Both trades generated substantial profits (+3.75R and +1.19R). However, T-101 only absorbed 0.30R of adverse excursion, proving pristine entry timing. In contrast, T-102 absorbed 0.94R of 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.
  2. The Execution-Aware Advantage in T-104: Notice Trade T-104: Because the parser registered the entry as CLOSE on 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.
  3. 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.12R loss. 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 DimensionAudit QuestionTarget BenchmarkCorrective Action
Winner MAE ConcentrationWhat percentage of winning trades experience an MAE < 0.50R?> 80% of winning tradesIf winners routinely absorb > 0.80R, refine entry filters or indicator lag.
Path Ambiguity HandlingAre entry bar extremes partitioned according to fill execution type?Zero phantom MAE on CLOSE fillsUse execution-aware bar slicing; do not naively include entire entry bar.
Normalized UnitsIs MAE audited in R-multiples alongside raw dollar drawdowns?Scale-independent R-auditReplace fixed dollar thresholds with volatility-adjusted (ATR) risk units.
Tail Loss BoundariesDo 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 IntegrityWas the stop loss parameter tested on untouched out-of-sample data?OOS profit factor and expectancy stableReject 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,&lt;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.