TradingView Backtesting

Pain Ratio: Return Above Threshold per Average Underwater Loss

Pain Ratio divides annualized return above a stated annual threshold by Pain Index, the time average of the depth below earlier equity peaks. The conventional threshold is a risk-free return. Pain Index uses drawdowns directly; Ulcer Index squares them before averaging and taking a square root. Both need the same equity history and observation rules for a meaningful comparison.

See the decision guide

Define underwater depth and the initial peak

Let E(t) be positive equity after costs, and H(t) the highest equity reached so far. Underwater depth is d(t) = 1 - E(t)/H(t), a nonnegative fraction. Over a window of duration T, Pain Index = (1/T) × integral of d(t) over time. A 10% drawdown held for twice as long contributes twice as much area. Include time at new highs, where depth is zero, in the total duration.

  • For n equally spaced period-end observations, use Hᵢ = max(E₀, E₁, ..., Eᵢ), dᵢ = 1 - Eᵢ/Hᵢ and Pain Index = sum(dᵢ)/n. E₀ seeds the peak but is not an extra averaging observation in this convention.
  • Starting the peak at the first period-end value can hide a loss in the first period. Starting at E₀ measures drawdown since the evaluation began. If the account entered the window below an earlier peak, supply that earlier high explicitly and disclose the carried-in drawdown.
  • This is an average over the whole window, not an average over losing trades, drawdown episodes, or only the dates below a peak. Removing zero depths changes the denominator and the question.

Match the return threshold to the evaluation window

Pain Ratio = (Rannual - τannual)/Pain Index. With no deposits or withdrawals and T measured in years, Rannual = (Eend/Estart)^(1/T) - 1. Zephyr defines the numerator using the difference between annualized investment and cash returns. A chosen hurdle can replace cash, but label that result as a hurdle-based variant.

  • The return, threshold and drawdown must use the same units: (0.12 - 0.02)/0.02083333 = (12 - 2)/2.083333 = 4.8. Display the ratio without a percent sign.
  • Use one start date, end date and equity basis for numerator and denominator. A full-history CAGR divided by a short rolling Pain Index does not describe the same sample.
  • For a variable cash benchmark, compound its total returns over the same dates, then annualize with the same T. For a fixed 2% effective annual threshold, a monthly equivalent is 1.02^(1/12) - 1. Do not subtract that monthly rate from annual CAGR.
  • Pain Index is a drawdown level, not periodic return volatility. Do not annualize it by multiplying by 12 or sqrt(12). Annualization belongs to the numerator; observation frequency determines which drawdowns the denominator sees.

Work through one equity path with both denominators

Assume initial equity of $100, no external cash flows, and 12 equally spaced valuations over exactly one year. All values are hypothetical and net of assumed costs. The monthly convention assigns equal time to each period; it is not an exact calendar-day weighting. The annual 2% threshold is an assumption, not a market quote.

  • Pain Index = 25/12 = 2.083333%. Ulcer Index = sqrt(225/12) = 4.330127%. Both include all 12 observations and use the same peaks.
  • Annual return = 112/100 - 1 = 12%. Excess return = 12 - 2 = 10 percentage points. Pain Ratio = 10/2.083333 = 4.8; the corresponding Ulcer Performance Index is 10/4.330127 = 2.309401.
  • Pain Index is the mean depth; Ulcer Index is its root mean square. On identical nonnegative depths and weights, Ulcer Index is at least as large as Pain Index. With positive excess return, Pain Ratio is therefore at least as large as the corresponding Ulcer ratio. This is an algebraic consequence, not superior performance.
Period endsEquityPeakDepthDepth sum / squared-depth sum
1$100$1000%0 / 0
2 and 3$90 each$10010% each20 pp / 200 pp²
4$95$1005%5 pp / 25 pp²
5 through 11$100 each$1000%0 / 0
12$112$1120%0 / 0
Total: 12 observationsStart $100; finish $112Maximum depth 10%25 pp total25 pp / 225 pp²

Separate drawdown depth from duration

Keep the same $100 start, $112 finish, one-year horizon and 2% threshold. Change only the intervening equity observations. All remaining period ends in the variants below are at $100, followed by $112 in period 12.

  • A and B have the same 10% maximum drawdown and terminal return. B spends one fewer observation at a 10% loss, so both average-loss measures fall.
  • A and C have equal underwater area and equal Pain Ratio. Ulcer Index penalizes A more because its losses are deeper. Neither statistic alone records the full chronology or the longest uninterrupted recovery time.
  • These deliberately simple monthly examples explain the arithmetic. Actual monthly sampling can hide a fall and recovery within a month. Use denser valuations when available and report the sampling frequency.
PathNonzero sampled depthsPain IndexUlcer IndexPain Ratio / Ulcer ratio
A: worked path10%, 10%, 5%2.083333%4.330127%4.800000 / 2.309401
B: faster recovery10%, 5%1.250000%3.227486%8.000000 / 3.098387
C: longer shallow loss5%, 5%, 5%, 5%, 5%2.083333%3.227486%4.800000 / 3.098387

Use time weights for irregular observations

For timestamps t₀ < t₁ < ... < tₙ, one explicit discrete convention is the right-endpoint estimate: Pain Index ≈ sum((tᵢ - tᵢ₋₁) × dᵢ)/(tₙ - t₀). Use the same weights inside the squared average for an Ulcer comparison. Each closing depth represents its preceding interval. This is a quadrature convention, not proof that equity stayed at that value throughout the interval.

  • Example: times in years [0, 0.2, 0.8, 1] and equity [100, 90, 90, 110] give depths [10%, 10%, 0%] and weights [0.2, 0.6, 0.2]. Pain Index = 8%; Ulcer Index = sqrt(80) = 8.944272%. An unweighted observation mean instead gives 6.666667%.
  • The example earns 10% over one year. Against 2%, the time-weighted Pain Ratio is 8/8 = 1 and the Ulcer ratio is 0.894427. Ignoring elapsed durations would give Pain Ratio 1.2.
  • A left-endpoint hold or linear interpolation describes a different assumed path and can produce a different area. Record the convention. For real dates, specify calendar time versus trading time and the day-count basis, such as elapsed days/365.25.
  • Irregular trade closures are not complete equity valuations. Duration weights cannot recover missing open-position losses or missing peaks. Prefer a regular mark-to-market account series; investigate gaps instead of silently dropping them or treating them as zero returns.

Run the complete calculation in Python

This Python 3 program uses only the standard library. The input is a cash-flow-adjusted equity index plus observation times expressed in years. It implements the right-endpoint convention above, seeds the peak with starting equity, and excludes the seed from averaging. All returned rates and depths are decimal fractions.

  • Expected main output: annual_return 0.12, annual_excess 0.10, pain_index 0.020833333, ulcer_index 0.043301270, pain_ratio 4.8, ulcer_ratio 2.309401077, observations 12, years 1.
  • An independent spreadsheet check uses the 12 depths in percentage points: AVERAGE(depths) = 25/12 and SQRT(SUMSQ(depths)/12) = sqrt(225/12). Dividing 10 by each denominator must reproduce the two ratios.
  • To supply dates, convert every timestamp to elapsed years from the first date using one stated day-count basis. The program does not infer missing valuations, remove cash flows, download prices, or validate accounting inputs.
Save as pain_ratio.py and run python3 pain_ratio.py. The embedded checks cover the worked path, duration weighting and zero drawdown.python
from math import expm1, fsum, isclose, isfinite, log, sqrt


def pain_metrics(equity, times_years, annual_threshold=0.02,
                 initial_peak=None):
    equity = [float(x) for x in equity]
    times = [float(x) for x in times_years]
    threshold = float(annual_threshold)
    if len(equity) != len(times) or len(equity) < 2:
        raise ValueError("Need a seed and matching observation times")
    if any(not isfinite(x) or x <= 0 for x in equity):
        raise ValueError("Equity must be finite and positive; report ruin separately")
    if any(not isfinite(t) for t in times):
        raise ValueError("Times must be finite")
    if not isfinite(threshold) or threshold <= -1:
        raise ValueError("Threshold must be an effective annual return above -100%")
    gaps = [b - a for a, b in zip(times, times[1:])]
    span = times[-1] - times[0]
    if any(not isfinite(g) or g <= 0 for g in gaps) or not isfinite(span):
        raise ValueError("Times must strictly increase over a finite window")
    peak = equity[0] if initial_peak is None else float(initial_peak)
    if not isfinite(peak) or peak < equity[0]:
        raise ValueError("Initial peak must be finite and at least starting equity")
    depths = []
    for value in equity[1:]:
        peak = max(peak, value)
        depths.append(max(0.0, 1.0 - value / peak))
    weights = [gap / span for gap in gaps]
    pain = fsum(w * d for w, d in zip(weights, depths))
    ulcer = sqrt(fsum(w * d * d for w, d in zip(weights, depths)))
    try:
        annual = expm1((log(equity[-1]) - log(equity[0])) / span)
    except OverflowError as error:
        raise ValueError("Annualized return exceeds numeric range") from error
    if not isfinite(annual):
        raise ValueError("Annualized return must be finite")
    excess = annual - threshold
    return {
        "annual_return": annual,
        "annual_excess": excess,
        "pain_index": pain,
        "ulcer_index": ulcer,
        "pain_ratio": None if pain == 0 else excess / pain,
        "ulcer_ratio": None if ulcer == 0 else excess / ulcer,
        "observations": len(depths),
        "years": span,
    }


if __name__ == "__main__":
    equity = [100, 100, 90, 90, 95] + [100] * 7 + [112]
    times = [i / 12 for i in range(13)]
    result = pain_metrics(equity, times)
    assert isclose(result["pain_index"], 0.25 / 12)
    assert isclose(result["ulcer_index"], sqrt(0.0225 / 12))
    assert isclose(result["pain_ratio"], 4.8)
    irregular = pain_metrics([100, 90, 90, 110], [0, 0.2, 0.8, 1])
    assert isclose(irregular["pain_index"], 0.08)
    assert isclose(irregular["pain_ratio"], 1.0)
    assert pain_metrics([100, 110], [0, 1])["pain_ratio"] is None
    for key, value in result.items():
        print(key, None if value is None else round(value, 9))

Recognize cases where the ratio misleads

Show the return, threshold, Pain Index and sample length beside the ratio. There is no universal good Pain Ratio cutoff, and a high value does not establish forecasting skill or executable returns.

  • Deposits can create artificial peaks and withdrawals can look like losses. Use a unitized or cash-flow-adjusted total-return index before calculating either drawdown or CAGR. Subtracting net deposits only from the final return does not repair the intermediate path.
  • Include marked open positions, cash, distributions, commissions and financing consistently. A smooth realized-profit curve can conceal large unrealized losses.
  • Pain Ratio differs from gain-to-pain ratio, which divides an arithmetic sum of returns by the absolute sum of negative returns. They have different denominators and cannot share a performance cutoff.
ConditionRequired interpretation
Pain Index = 0The ratio is undefined, including 0/0. The program returns None. Sparse samples can hide actual losses.
Positive Pain Index; return equals thresholdPain Ratio is genuinely zero. This differs from a software zero-denominator fallback.
Negative excess returnWith -2 pp excess, Pain Index 1% gives -2; Pain Index 2% gives -1. A higher negative ratio can merely reflect more drawdown.
Near-zero denominatorRounding or one missed valuation can dominate the ratio. Show full precision and the underlying curve.
Equity reaches zero or becomes negativeThe code rejects the path. Report total loss, insolvency and any recapitalization separately; a normal CAGR comparison is unsuitable.
Very short sampleAnnualized growth can be enormous after a brief gain. Report actual dates and test on subsequent data rather than interpreting annualization as a year of evidence.

Reconcile TradingView inputs before using the report

TradingView exports each Strategy Report tab separately. A List of Trades CSV contains trade information; the Performance Summary export contains metrics. Backtest Deep Report parses the trade CSV and builds its equity curve from initial capital plus imported closed-trade net P&L. That event curve is not a complete sequence of account valuations.

  • The inspected report implementation includes Ulcer Index and Ulcer Performance Index, but no dedicated Pain Ratio. Its Ulcer average includes the starting point and weights trade events equally. It therefore need not match the regular or duration-weighted examples here.
  • Use Backtest Deep Report to reconcile supported trade and risk results. Obtain a separate marked equity series for this Pain Ratio calculation, and label its time window, cash-flow treatment, peak seed and sampling rules.
  • Pine Script AI Coding Agent can help draft custom strategy code. Specify the equity observations and boundary behavior, then review and validate the output and reconcile it with a known calculation. This page supplies Python research code, not a Pineify built-in Pain Ratio feature.
Where Pineify fits

Backtest Deep Report

Import TradingView trade CSV data and inspect supported trade and risk metrics. Calculate the time-weighted Pain Ratio separately from suitable equity valuations.

Also useful: Pine Script AI Coding Agent. Describe custom strategy rules and equity sampling requirements, then review and validate the generated code against a known example.

Review exported trades

Frequently asked questions

Educational information, not investment advice. The equity paths and return threshold are hypothetical. Historical ratios, sampled drawdowns and annualized backtest returns do not guarantee future results.

Sources and verification

Published by Pineify. Pineify is an independent product and is not affiliated with TradingView. Sources support the technical explanations; examples do not establish trading performance.