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?
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:
- 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.
- Path inconsistency: Extracting net profit from closed-trade ledgers while computing drawdown from bar-by-bar mark-to-market equity curves, or vice versa.
- 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.
- Zero-drawdown division: Failing to handle monotonic or short evaluation windows where maximum drawdown is zero, causing unhandled
ZeroDivisionErrorcrashes 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:
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:
Total Net Profit ($) = E_T - E_0The running High-Water Mark (HWM_t) and the currency drawdown series (DD_{curr, t}) are defined as:
HWM_t = max_{0 <= s <= t} (E_s)
DD_{curr, t} = HWM_t - E_tThe maximum absolute drawdown over the full backtest horizon is:
Max DD ($) = max_{0 <= t <= T} (DD_{curr, t})Provided that Max DD ($) > 0, the currency recovery factor is:
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:
Recovery Factor (%) = Cumulative Return (%) / Maximum Percentage Drawdown (%)Here, the cumulative percentage return is normalized against initial starting capital E_0:
Cumulative Return (%) = (E_T - E_0) / E_0The relative percentage drawdown at bar t measures the decline relative to the peak equity achieved up to that point:
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:
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:
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.40RF_{%} 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:
CAGR = (E_T / E_0)^(1 / Y) - 1
Calmar Ratio = CAGR / Maximum Relative Drawdown (%)Where Y is the elapsed evaluation time in fractional years:
Y = Total Calendar Days / 365.25Revisiting 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:
| Metric | Numerator Unit | Denominator Unit | Time Normalized? | Sensitivity to Test Length | Primary Analytical Function |
|---|---|---|---|---|---|
| Recovery Factor ($) | Currency (E_T - E_0) | Currency (max(HWM - E)) | No (Cumulative) | Scales with backtest duration | Cash-in-pocket recovery auditing |
| Recovery Factor (%) | Percentage ((E_T - E_0) / E_0) | Percentage (max((HWM - E)/HWM)) | No (Cumulative) | Scales with backtest duration | Dimensionless return-to-pain auditing |
| Calmar Ratio | Annualized Return (CAGR) | Percentage (Max DD %) | Yes (Annualized) | Invariant to test duration | Multi-strategy cross-horizon ranking |
| Sterling Ratio | Annualized Return (CAGR) | Average Annual Max DD + Arbitrary Buffer | Yes (Annualized) | Invariant to test duration | Institutional mandate compliance |
| Sharpe Ratio | Annualized Excess Return | Annualized Total Volatility (sigma) | Yes (Square-root rule) | Invariant to test duration | Total 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.
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_scalarsand returnsnp.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:
Net Profit = -$15,000
Max DD = $30,000
RF = -$15,000 / $30,000 = -0.50A 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.5after 3 years. - An expected Recovery Factor of
~5.0after 10 years. - An expected Recovery Factor of
~10.0after 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.
"""
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:
============================================================
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:
- In Case A,
Recovery Factor (Currency)is0.7349, whileRecovery Factor (Percentage)is1.0504. Because the drawdown occurred after the account had appreciated from its initial$100,000capital, the dollar loss ($53,081.67) represents a larger burden in dollar terms than the relative peak-to-trough percentage (37.14%). - Over the 2.89-year duration, the annualized
Calmar Ratiois0.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. - In Case B, the zero-drawdown edge case cleanly outputs
Infinity (Zero DD)without triggering exceptions. - 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:
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
ZeroDivisionErrorcrashes 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?
