The anatomy of recovery factor: Quantifying net yield against peak trauma

In systematic trading research, aggregate net profit is one of the most deceptive headline metrics an engineer can inspect. A quantitative strategy generating $150,000 in net profit while suffering a harrowing peak-to-trough capital decline of $120,000 represents an entirely different risk profile than an identical system generating $150,000 with a maximum equity drawdown of $15,000. The first system operated on the knife-edge of account liquidation and psychological abandonment; the second compounded capital with high structural resilience.

To capture this relationship in a single summary statistic, quantitative analysts rely on the recovery factor. In algorithmic recovery factor trading, this metric measures the efficiency with which a strategy converts historical balance trauma into cumulative net profit. It answers an essential capital allocation question: For every dollar or percentage point conceded during the strategy's worst historical drawdown, how many units of total profit did the system return over its entire operational lifecycle?

Code
flowchart TD
    A["Mark-to-Market Equity Ledger E_t"] --> B["Compute High-Water Mark HWM_t"]
    B --> C["Extract Absolute Net Profit: E_T - E_0"]
    B --> D["Extract Peak-to-Trough Drawdown Series"]
    D --> E["Identify Maximum Drawdown"]
    C --> F["Unit Alignment Guard"]
    E --> F
    F --> G["Currency Ledger ($)\nNet Profit ($) / Max Drawdown ($)"]
    F --> H["Percentage Ledger (%)\nCum Return (%) / Max Drawdown (%)"]
    G --> I["Recovery Factor Metric"]
    H --> I
    I --> J["Annualization Engine (CAGR / Max DD)"]
    J --> K["Calmar Ratio"]

While the conceptual definition is straightforward, calculating recovery factor in a production recovery factor backtest is rife with subtle accounting traps. Software libraries and custom backtesting scripts frequently make fatal errors:

  1. Unit mismatching: Dividing an absolute currency net profit (such as $45,000) by a relative percentage drawdown (such as 18%), producing a mathematically nonsensical quotient.
  2. Path inconsistency: Extracting net profit from closed-trade ledgers while computing drawdown from bar-by-bar mark-to-market equity curves, or vice versa.
  3. Time-horizon distortion: Evaluating cumulative recovery factor across different backtest durations without recognizing that recovery factor is an unannualized metric, unlike the Sharpe, Sortino, and Calmar ratios.
  4. Zero-drawdown division: Failing to handle monotonic or short evaluation windows where maximum drawdown is zero, causing unhandled ZeroDivisionError crashes or silent infinity propagation in downstream optimization sweeps.

This guide details the mathematical foundations of recovery factor, resolves the divergence between currency and percentage ledgers, formalizes edge-case handling, and delivers a production-grade Python implementation.


The unit matching rule: Currency vs percentage formulations

The cardinal rule of recovery factor calculation is unit consistency. The numerator (net gain) and the denominator (maximum historical deficit) must reside within the exact same dimension and accounting frame.

1. Currency formulation ($)

When tracking portfolio performance in absolute dollar terms, the currency recovery factor quantifies dollar earnings per dollar of maximum historical capital impairment:

Code
Recovery Factor ($) = Total Net Profit ($) / Maximum Absolute Drawdown ($)

Formally, let E_t represent portfolio equity at discrete bar t \in [0, T], where E_0 is initial account capital and E_T is terminal equity.

The numerator is the total net profit:

Code
Total Net Profit ($) = E_T - E_0

The running High-Water Mark (HWM_t) and the currency drawdown series (DD_{curr, t}) are defined as:

Code
HWM_t = max_{0 <= s <= t} (E_s)
DD_{curr, t} = HWM_t - E_t

The maximum absolute drawdown over the full backtest horizon is:

Code
Max DD ($) = max_{0 <= t <= T} (DD_{curr, t})

Provided that Max DD ($) > 0, the currency recovery factor is:

Code
RF_{$} = (E_T - E_0) / max_{0 <= t <= T} (HWM_t - E_t)

2. Percentage formulation (%)

When evaluating strategies across varying account sizes or assessing compounding systems, practitioners calculate recovery factor on a normalized percentage basis:

Code
Recovery Factor (%) = Cumulative Return (%) / Maximum Percentage Drawdown (%)

Here, the cumulative percentage return is normalized against initial starting capital E_0:

Code
Cumulative Return (%) = (E_T - E_0) / E_0

The relative percentage drawdown at bar t measures the decline relative to the peak equity achieved up to that point:

Code
DD_{pct, t} = (HWM_t - E_t) / HWM_t
Max DD (%) = max_{0 <= t <= T} (DD_{pct, t})

Provided Max DD (%) > 0, the percentage recovery factor is:

Code
RF_{%} = [ (E_T - E_0) / E_0 ] / max_{0 <= t <= T} [ (HWM_t - E_t) / HWM_t ]

The mathematical divergence: Why Currency RF != Percentage RF

A frequent source of confusion among algorithmic engineers is that RF_{$} and RF_{%} do not yield the same numerical value on the exact same equity sequence.

Consider a strategy starting with E_0 = $100,000. Over two years, the equity path unfolds as follows:

  • Equity climbs steadily to a peak of $200,000 (HWM = $200,000).
  • The market enters an adverse regime; equity falls to $150,000.
  • The strategy recovers and finishes at terminal equity E_T = $210,000.

Let us compute both metrics:

Code
Total Net Profit ($) = $210,000 - $100,000 = $110,000
Cumulative Return (%) = $110,000 / $100,000 = 110.0%

Max Drawdown ($) = $200,000 - $150,000 = $50,000
Max Drawdown (%) = ($200,000 - $150,000) / $200,000 = 25.0%

RF_{$} = $110,000 / $50,000 = 2.20
RF_{%} = 110.0% / 25.0% = 4.40

RF_{%} is exactly double RF_{$}. Why does this discrepancy occur?

The denominator of RF_{%} normalizes the drawdown against the contemporary peak ($200,000), whereas the numerator normalizes the return against the original capital base ($100,000). In a growing compounding account, a 25% drawdown at a $200,000 peak represents $50,000 of loss—equivalent to 50% of the initial capital base.

Neither formulation is inherently incorrect, but they answer different questions:

  • Currency RF measures absolute dollar recovery against the single deepest monetary cash reduction experienced by the account.
  • Percentage RF measures normalized compounding return against the deepest proportional drop from a peak.

Mixing the two—such as dividing $110,000 by 25.0% (yielding 4,400) or dividing 110.0% by $50,000 (yielding 0.0022%)—destroys the mathematical integrity of your reporting suite.


Time-dependence: Recovery factor vs Calmar ratio

One of the most consequential methodological errors in comparative strategy research is contrasting recovery factors across backtests of unequal durations. Recovery factor is an unannualized cumulative metric. It aggregates every dollar of net profit earned over the entire test horizon against the single deepest peak-to-trough trough.

The duration trap

Examine two hypothetical trading strategies evaluated over different backtest horizons:

  • Strategy Alpha: Evaluated over 1 year. Generates a net return of 30% with a maximum peak-to-trough drawdown of 10%.
    Code
    RF_{Alpha} = 30% / 10% = 3.0
  • Strategy Beta: Evaluated over 10 years. Generates a cumulative net return of 250% with a maximum peak-to-trough drawdown of 50%.
    Code
    RF_{Beta} = 250% / 50% = 5.0

A superficial query comparing headline recovery factors would conclude that Strategy Beta (RF = 5.0) is superior to Strategy Alpha (RF = 3.0).

In reality, Strategy Beta required an entire decade to generate 5 units of drawdown recovery (averaging 0.50 recovery units per annum), while Strategy Alpha produced 3 units of drawdown recovery in just 12 months. Strategy Beta subjected capital to a devastating 50% drawdown—a deficit requiring a 100% gain just to break even, as detailed in our guide on maximum drawdown in Python.

Formalizing recovery factor vs Calmar

To evaluate drawdown resilience independently of test duration, quants turn to the Calmar ratio. The Calmar ratio annualizes the return numerator using the Compound Annual Growth Rate (CAGR) before dividing by the maximum percentage drawdown:

Code
CAGR = (E_T / E_0)^(1 / Y) - 1
Calmar Ratio = CAGR / Maximum Relative Drawdown (%)

Where Y is the elapsed evaluation time in fractional years:

Code
Y = Total Calendar Days / 365.25

Revisiting our two strategies under the Calmar lens:

  • Strategy Alpha (1 Year):
    Code
    CAGR = (1.30)^(1/1) - 1 = 30.0%
    Calmar = 30.0% / 10.0% = 3.00
  • Strategy Beta (10 Years):
    Code
    CAGR = (3.50)^(1/10) - 1 = 13.35%
    Calmar = 13.35% / 50.0% = 0.267

When normalized for time, Strategy Alpha's capital efficiency is more than ten times higher than Strategy Beta's.

Methodological comparison matrix

The table below delineates the structural differences across key drawdown-adjusted performance ratios:

MetricNumerator UnitDenominator UnitTime Normalized?Sensitivity to Test LengthPrimary Analytical Function
Recovery Factor ($)Currency (E_T - E_0)Currency (max(HWM - E))No (Cumulative)Scales with backtest durationCash-in-pocket recovery auditing
Recovery Factor (%)Percentage ((E_T - E_0) / E_0)Percentage (max((HWM - E)/HWM))No (Cumulative)Scales with backtest durationDimensionless return-to-pain auditing
Calmar RatioAnnualized Return (CAGR)Percentage (Max DD %)Yes (Annualized)Invariant to test durationMulti-strategy cross-horizon ranking
Sterling RatioAnnualized Return (CAGR)Average Annual Max DD + Arbitrary BufferYes (Annualized)Invariant to test durationInstitutional mandate compliance
Sharpe RatioAnnualized Excess ReturnAnnualized Total Volatility (sigma)Yes (Square-root rule)Invariant to test durationTotal variance risk-adjusted efficiency

[!TIP] Use Recovery Factor when auditing single-trial performance on fixed historical periods or assessing whether a strategy has fully healed from a specific historical crisis. Use the Calmar ratio whenever comparing systems across unequal sample lengths or conducting walk-forward optimizations.


Boundary conditions and edge cases: Zero drawdown, losses, and ruin

In algorithmic backtesting pipelines, mathematical definitions must withstand pathological edge cases without throwing unhandled exceptions or corrupting vector arrays.

Code
flowchart TD
    A["Raw Account Series"] --> B{"Sample Size >= 2?"}
    B -- No --> C["Raise ValueError: Insufficient Data"]
    B -- Yes --> D{"Is Max DD == 0?"}
    D -- Yes --> E{"Net Profit > 0?"}
    E -- Yes --> F["Return +Infinity (Flag: Zero DD Monotonic)"]
    E -- No --> G["Return 0.0 (Flag: Flat Inactive Curve)"]
    D -- No --> H{"Net Profit < 0?"}
    H -- Yes --> I["Negative Recovery Factor (Net Deficit)"]
    H -- No --> J{"Terminal Equity <= 0?"}
    J -- Yes --> K["Ruin / Liquidation (CAGR = -100%)"]
    J -- No --> L["Standard Nominal Recovery Factor"]

1. The zero-drawdown singularity (Max DD = 0)

When a backtest executes over a brief window, or when an aggressive filter executes only a handful of winning trades with zero mark-to-market pullbacks, the maximum drawdown is exactly zero (Max DD = 0.0).

Dividing net profit by zero yields an undefined mathematical result (N / 0 \to \infty). If unhandled in Python:

  • Native arithmetic throws a fatal ZeroDivisionError: float division by zero.
  • NumPy emits a RuntimeWarning: divide by zero encountered in double_scalars and returns np.inf.

Unbounded infinities cause catastrophic errors in automated parameter sweeps:

  • Genetic algorithms assign infinite fitness scores to unviable single-trade edge cases.
  • Mean-variance optimizers crash when computing covariance matrices containing non-finite elements.
  • SQL persistence layers reject non-compliant IEEE floating-point values when storing trial metrics.

Production Defense: A robust backtesting engine must check whether max_drawdown == 0.0. If true, the system should verify whether total_net_profit > 0.0 and return an explicit sentinel value (such as float('inf') accompanied by an explicit metadata warning flag) or assign np.nan if the sample size fails minimum observation thresholds.

2. Negative net profit (Net Profit < 0)

If a strategy loses money over the backtest window (E_T < E_0), the numerator is negative. Because maximum drawdown is by definition a non-negative distance (Max DD >= 0), the resulting recovery factor is negative:

Code
Net Profit = -$15,000
Max DD = $30,000
RF = -$15,000 / $30,000 = -0.50

A negative recovery factor indicates that the system destroyed capital, and its terminal loss equals 50% of the maximum depth it experienced during its worst drawdown. When ranking strategies, negative recovery factors correctly sort below zero, but researchers must ensure that an optimization engine does not favor a system with a larger negative drawdown simply because it reduces the absolute magnitude of the negative quotient (e.g., -10 / 50 = -0.20 vs -10 / 20 = -0.50).

3. Account ruin and liquidation (E_t <= 0)

If a leveraged strategy suffers a drawdown equal to or exceeding 100% of peak capital, the account is liquidated. At this point, subsequent equity values are zero or negative:

  • In percentage terms, Max DD (%) = 100.0%.
  • Terminal equity E_T = 0.
  • Cumulative return is -100.0%.
  • The recovery factor evaluates to -1.00.

In backtesting engines, any equity series touching or crossing zero must trigger an immediate operational halt. Computing subsequent metrics on synthetic negative balances distorts compounding curves and invalidates time-weighted return calculations, as explored in our guide on time-weighted return in Python.

4. Account path consistency: Closed trades vs mark-to-market ledgers

A pervasive flaw in retail backtest reporting is calculating recovery factor by taking the sum of closed trades from a trade log and dividing it by an intraday mark-to-market drawdown extracted from high-frequency equity ticks.

Closed-trade sequences hide Maximum Adverse Excursion (MAE). A trade that is entered, suffers an unrealized 40% intraday loss, and subsequently bounces back to close with a 1% profit records a positive gain in trade-level ledgers. If your drawdown calculation ignores open positions, your reported recovery factor will be artificially inflated by an order of magnitude.

[!IMPORTANT] Always compute both Total Net Profit and Maximum Drawdown from the exact same underlying mark-to-market equity path. Both metrics must be derived from bar-by-bar causal valuation records.


Debunking arbitrary "good vs bad" thresholds

Online trading forums frequently circulate rigid heuristic thresholds for recovery factor: "A recovery factor above 3.0 is viable; anything below 1.5 should be discarded."

Applying universal thresholds without context is unscientific and actively harmful to quantitative strategy selection. The nominal recovery factor of a viable trading system is fundamentally governed by four structural dimensions:

1. Backtest time horizon

As established mathematically, recovery factor accumulates with time. A trend-following strategy with an annual Calmar ratio of 0.50 will exhibit:

  • An expected Recovery Factor of ~1.5 after 3 years.
  • An expected Recovery Factor of ~5.0 after 10 years.
  • An expected Recovery Factor of ~10.0 after 20 years.

Discarding the 3-year model because its recovery factor is below 3.0 penalizes the strategy solely for having a shorter historical observation window.

2. Trade frequency and holding period

High-frequency and intraday momentum strategies hold positions for minutes to hours, experiencing rapid turnover with tightly constrained overnight risk. These systems routinely generate recovery factors exceeding 10.0 over modest time horizons due to shallow peak-to-trough drawdowns.

Conversely, long-term multi-week trend-following systems on volatile commodities often endure extended multi-quarter consolidation drawdowns. A recovery factor of 2.5 over a 10-year cycle in physical commodities may represent top-decile institutional alpha.

3. Reinvestment mechanics (Fixed-lot vs Compounding)

In a fixed-lot backtest (where position size is constant regardless of account balance), equity grows linearly. In a compounding backtest (where position size scales proportionally with equity), equity grows exponentially while drawdown depth scales with account size. The resulting recovery factor profiles diverge substantially across the two regimes.

Rigorous validation alternatives

Rather than filtering models against arbitrary static cutoffs, rigorous quantitative research relies on:

  • Peer-group distribution ranking: Comparing a strategy's recovery factor against the empirical distribution of randomized zero-alpha Monte Carlo permutations.
  • Rolling walk-forward recovery stability: Measuring whether the recovery factor remains stable across rolling out-of-sample windows rather than depending on a single lucky post-crisis rebound.
  • Expectancy alignment: Verifying that recovery factor gains are supported by positive mathematical expectancy, as detailed in our analysis of trading expectancy in Python.

Complete Python implementation: Auditing ledgers and metric reconciliation

The production-grade Python script below provides an end-to-end framework for auditing recovery factor. It implements:

  • Dual-ledger computation (absolute currency and relative percentage).
  • High-Water Mark tracking with peak-to-trough drawdown extraction.
  • Annualized CAGR and Calmar ratio calculation.
  • Explicit defensive guards for zero-drawdown, negative-return, and insufficient-sample boundary conditions.
  • Structured tabular reporting.
Code
"""
Recovery Factor Reconciliation Script
Auditing Currency vs Percentage Formulations, Calmar Comparison, and Edge Cases
"""

import numpy as np
import pandas as pd


def audit_recovery_factor(
    equity: pd.Series,
    initial_capital: float | None = None,
    annual_trading_days: int = 252,
) -> dict[str, float | str]:
    """
    Computes strict currency and percentage recovery factors along with
    annualized Calmar ratio from a single continuous mark-to-market equity curve.
    """
    if len(equity) < 2:
        raise ValueError("Equity series must contain at least 2 observations.")

    e_0 = float(equity.iloc[0]) if initial_capital is None else float(initial_capital)
    e_t = float(equity.iloc[-1])
    total_net_profit_curr = e_t - e_0
    cumulative_return_pct = total_net_profit_curr / e_0

    # High-water mark tracking
    hwm = equity.cummax()

    # Currency drawdown series ($)
    dd_curr = hwm - equity
    max_dd_curr = float(dd_curr.max())

    # Percentage drawdown series (%) relative to instantaneous peak
    dd_pct = (hwm - equity) / hwm
    max_dd_pct = float(dd_pct.max())

    # 1. Recovery Factor (Currency)
    if max_dd_curr > 0:
        rf_curr = total_net_profit_curr / max_dd_curr
    elif max_dd_curr == 0.0:
        rf_curr = float("inf") if total_net_profit_curr > 0 else 0.0
    else:
        rf_curr = np.nan

    # 2. Recovery Factor (Percentage)
    if max_dd_pct > 0:
        rf_pct = cumulative_return_pct / max_dd_pct
    elif max_dd_pct == 0.0:
        rf_pct = float("inf") if cumulative_return_pct > 0 else 0.0
    else:
        rf_pct = np.nan

    # 3. Annualization Horizon & Calmar Ratio
    if isinstance(equity.index, pd.DatetimeIndex):
        calendar_days = (equity.index[-1] - equity.index[0]).days
        years = max(calendar_days / 365.25, 1.0 / 365.25)
    else:
        years = max(len(equity) / float(annual_trading_days), 1.0 / annual_trading_days)

    if e_t > 0:
        cagr = (e_t / e_0) ** (1.0 / years) - 1.0
    else:
        cagr = -1.0

    if max_dd_pct > 0:
        calmar = cagr / max_dd_pct
    elif max_dd_pct == 0.0:
        calmar = float("inf") if cagr > 0 else 0.0
    else:
        calmar = np.nan

    return {
        "Initial Capital ($)": e_0,
        "Final Equity ($)": e_t,
        "Total Net Profit ($)": total_net_profit_curr,
        "Cumulative Return (%)": cumulative_return_pct * 100.0,
        "Max Drawdown ($)": max_dd_curr,
        "Max Drawdown (%)": max_dd_pct * 100.0,
        "Recovery Factor (Currency)": rf_curr,
        "Recovery Factor (Percentage)": rf_pct,
        "Evaluation Years": years,
        "CAGR (%)": cagr * 100.0,
        "Annualized Calmar Ratio": calmar,
    }


def print_comparison_table(title: str, results: dict[str, float | str]) -> None:
    print(f"\n{'=' * 60}")
    print(f" {title.upper()}")
    print(f"{'=' * 60}")
    for metric, val in results.items():
        if isinstance(val, float):
            if np.isinf(val):
                formatted = "Infinity (Zero DD)"
            elif np.isnan(val):
                formatted = "NaN (Undefined)"
            elif "$" in metric:
                formatted = f"${val:14,.2f}"
            elif "%" in metric:
                formatted = f"{val:13.2f}%"
            else:
                formatted = f"{val:14.4f}"
        else:
            formatted = str(val)
        print(f"  {metric:<30} | {formatted:>18}")
    print(f"{'=' * 60}")


if __name__ == "__main__":
    # Case A: Standard 3-Year Trend Strategy
    np.random.seed(101)
    dates_3y = pd.date_range("2023-01-01", periods=756, freq="B")
    initial_cap = 100_000.0

    # Stochastic drift with mid-lifecycle correction
    rets = np.random.normal(0.0008, 0.012, size=756)
    rets[250:290] = -0.011  # Controlled drawdown period
    rets[290:350] = 0.004   # Recovery phase

    eq_path_standard = initial_cap * np.cumprod(1 + rets)
    eq_series_standard = pd.Series(eq_path_standard, index=dates_3y)
    res_standard = audit_recovery_factor(eq_series_standard, initial_cap)
    print_comparison_table("Case A: Standard 3-Year Backtest", res_standard)

    # Case B: Zero Drawdown Toy Sequence (Strictly Monotonic)
    dates_short = pd.date_range("2026-01-01", periods=10, freq="B")
    eq_monotonic = pd.Series(np.linspace(100_000, 105_000, 10), index=dates_short)
    res_zero_dd = audit_recovery_factor(eq_monotonic, 100_000.0)
    print_comparison_table("Case B: Monotonic Upward Curve (Zero Drawdown)", res_zero_dd)

    # Case C: Strategy with Negative Net Profit
    eq_losing = pd.Series(np.linspace(100_000, 85_000, 100), index=dates_3y[:100])
    res_losing = audit_recovery_factor(eq_losing, 100_000.0)
    print_comparison_table("Case C: Net Deficit Curve (Negative Profit)", res_losing)

Script execution output

Running the reconciliation script outputs exact metrics for each operational scenario:

Code
============================================================
 CASE A: STANDARD 3-YEAR BACKTEST
============================================================
  Initial Capital ($)            |    $    100,000.00
  Final Equity ($)               |    $    139,007.44
  Total Net Profit ($)           |    $     39,007.44
  Cumulative Return (%)          |             39.01%
  Max Drawdown ($)               |    $     53,081.67
  Max Drawdown (%)               |             37.14%
  Recovery Factor (Currency)     |             0.7349
  Recovery Factor (Percentage)   |             1.0504
  Evaluation Years               |             2.8939
  CAGR (%)                       |             12.05%
  Annualized Calmar Ratio        |             0.3246
============================================================

============================================================
 CASE B: MONOTONIC UPWARD CURVE (ZERO DRAWDOWN)
============================================================
  Initial Capital ($)            |    $    100,000.00
  Final Equity ($)               |    $    105,000.00
  Total Net Profit ($)           |    $      5,000.00
  Cumulative Return (%)          |              5.00%
  Max Drawdown ($)               |    $          0.00
  Max Drawdown (%)               |              0.00%
  Recovery Factor (Currency)     | Infinity (Zero DD)
  Recovery Factor (Percentage)   | Infinity (Zero DD)
  Evaluation Years               |             0.0356
  CAGR (%)                       |            293.86%
  Annualized Calmar Ratio        | Infinity (Zero DD)
============================================================

============================================================
 CASE C: NET DEFICIT CURVE (NEGATIVE PROFIT)
============================================================
  Initial Capital ($)            |    $    100,000.00
  Final Equity ($)               |    $     85,000.00
  Total Net Profit ($)           |    $    -15,000.00
  Cumulative Return (%)          |            -15.00%
  Max Drawdown ($)               |    $     15,000.00
  Max Drawdown (%)               |             15.00%
  Recovery Factor (Currency)     |            -1.0000
  Recovery Factor (Percentage)   |            -1.0000
  Evaluation Years               |             0.3751
  CAGR (%)                       |            -35.16%
  Annualized Calmar Ratio        |            -2.3442
============================================================

Notice the key takeaways from the audited reconciliation:

  1. In Case A, Recovery Factor (Currency) is 0.7349, while Recovery Factor (Percentage) is 1.0504. Because the drawdown occurred after the account had appreciated from its initial $100,000 capital, the dollar loss ($53,081.67) represents a larger burden in dollar terms than the relative peak-to-trough percentage (37.14%).
  2. Over the 2.89-year duration, the annualized Calmar Ratio is 0.3246. This reconciles with the cumulative percentage recovery factor (1.0504 / 2.89 \approx 0.36), with the remaining difference driven by non-linear annual compounding in CAGR.
  3. In Case B, the zero-drawdown edge case cleanly outputs Infinity (Zero DD) without triggering exceptions.
  4. In Case C, the net-loss curve outputs -1.0000, confirming that the system lost exactly 100% of the depth of its maximum drawdown.

Architectural integration in OpenBacktesting

In institutional quantitative research, metrics must not exist as isolated post-hoc calculations. They must be direct mathematical projections of an inspectable, reproducible execution graph.

OpenBacktesting (0.2.1), built for Python >=3.12,<3.15, is an independent single-asset historical OHLCV bar-by-bar research engine designed around execution auditability:

Code
flowchart LR
    A["Historical OHLCV Bar Feed"] --> B["Causal Event Loop\n(No Lookahead Bias)"]
    B --> C["Order & Fill Generator"]
    C --> D["Bar-Close Mark-to-Market Accounting"]
    D --> E["SQLite Ledger Store (trials.db)"]
    E --> F["Audited Trial Metrics\n(RF_$, RF_%, Calmar, MDD)"]
    E --> G["OpenBacktesting Studio UI\n(Underwater Curve Inspection)"]

1. Causal mark-to-market accounting

Rather than recording equity changes only when positions close, OpenBacktesting computes the portfolio's mark-to-market valuation at every single bar close. Slippage, commissions, and intrabar adverse excursions are recorded deterministically. This guarantees that both the numerator (Total Net Profit) and the denominator (Maximum Drawdown) are extracted from the identical continuous account ledger.

2. SQLite persistence (trials.db)

Every backtest iteration executes as an immutable trial record stored in local SQLite (trials.db). Strategy parameters, executed order tickets, individual fills, and bar-by-bar equity series are permanently persisted. Researchers can query historical runs, run cross-trial metric audits, and verify that recovery factors were not distorted by anomalous single-tick artifacts.

3. Studio UI visual inspection

High headline recovery factors can easily mask undesirable structural behavior, such as a strategy that spends 90% of its lifespan underwater before experiencing a sudden, unrepeatable profit spike on the final 20 bars.

Through OpenBacktesting's local Studio UI, researchers inspect the full underwater drawdown chart alongside the cumulative equity curve. This visual audit immediately reveals whether a recovery factor was earned through sustained, repeatable capital recovery or through a single high-leverage market tail event.


Pre-flight audit checklist for quantitative researchers

Before presenting backtest results to risk committees, allocators, or deploying strategy code into production, verify your recovery factor calculations against this engineering checklist:

  • Unit Calibration: Is the numerator in currency ($) divided by maximum drawdown in currency ($), OR is cumulative percentage return divided by maximum percentage drawdown? Have you eliminated any mixed-unit calculations?
  • Single Account Path: Are both net profit and maximum drawdown derived from the exact same bar-by-bar mark-to-market equity curve, rather than combining closed-trade logs with continuous price series?
  • Time Normalization: If comparing strategies with different backtest date ranges or varying sample lengths, have you benchmarked the annualized Calmar ratio alongside cumulative recovery factor?
  • Zero-Drawdown Sentinel: Does your calculation script feature defensive guards that prevent unhandled ZeroDivisionError crashes and flag monotonic toy sequences?
  • MAE Integrity: Does your drawdown engine capture intra-trade adverse excursions, or does it only sample equity at closed-trade boundaries?
  • Negative Return Behavior: If total net profit is negative, does your pipeline preserve the negative sign and avoid ranking large drawdowns ahead of small drawdowns?
  • Persistent Audit Trail: Is the underlying equity path persisted in a queryable trial database (such as OpenBacktesting's trials.db) so that historical metrics can be audited bar by bar?