TradingView Backtesting

Upside Potential Ratio: Target Return and Downside Denominator

The upside potential ratio (UPR) divides the first upper partial moment above a target return by the square root of the second lower partial moment below it. Using all n observations in both averages, UPR = mean(max(r - target, 0)) / sqrt(mean(min(r - target, 0)²)). It measures observed upside relative to target shortfalls. The numerator excludes shortfalls, unlike the mean excess return in a Sortino ratio.

See the decision guide

Define both partial moments before comparing results

Let t be a fixed minimum acceptable return (MAR), rᵢ the return in period i, and n the number of valid periods. Define uᵢ = max(rᵢ - t, 0) and dᵢ² = min(rᵢ - t, 0)². The full-sample estimate uses UPM₁ = sum(uᵢ)/n, LPM₂ = sum(dᵢ²)/n and UPR = UPM₁/sqrt(LPM₂), provided LPM₂ > 0. The divisor is n, not n - 1: these are empirical partial moments about a specified target, not sample variance about an estimated mean.

  • Plantinga and de Groot give this full-sample formula in equation (5) of their 2001 paper, with n defined as the number of periods. The earlier Dutch Triangle paper by Sortino, van der Meer and Plantinga introduced the approach in 1999. Both sources are linked below.
  • Count n₊ = number of rᵢ > t, n₋ = number of rᵢ < t and n₀ = number of rᵢ = t. Ties contribute zero to both sums but remain in n.
  • UPM₁ has return units. LPM₂ has squared-return units. Its square root has return units, so UPR is dimensionless. Express the target and returns consistently as decimals or percentage points.

Work every observation at three monthly targets

Assume six consecutive hypothetical monthly account returns after costs, without deposits or withdrawals. Series A is -4%, -2%, 0%, 1%, 3%, 6%. Each cell below shows uᵢ in percentage points, then dᵢ² in squared percentage points. For example, a -4% return against a 1% target has excess -5 pp, max(-5, 0) = 0 and min(-5, 0)² = 25.

  • At t = 0%, UPM₁ = 10/6 = 1.666667 pp; LPM₂ = 20/6 = 3.333333 pp²; downside deviation = sqrt(20/6) = 1.825742 pp; UPR = 0.912871.
  • At t = 1%, UPM₁ = 7/6 = 1.166667 pp; LPM₂ = 35/6 = 5.833333 pp²; downside deviation = 2.415229 pp; UPR = 0.483046.
  • At t = 3%, UPM₁ = 3/6 = 0.500000 pp; LPM₂ = 87/6 = 14.500000 pp²; downside deviation = 3.807887 pp; UPR = 0.131306. Rounded display values are not used as calculation inputs.
MonthReturnTarget 0%: uᵢ / dᵢ²Target 1%: uᵢ / dᵢ²Target 3%: uᵢ / dᵢ²
1-4%0 / 160 / 250 / 49
2-2%0 / 40 / 90 / 25
30%0 / 00 / 10 / 9
41%1 / 00 / 00 / 4
53%3 / 02 / 00 / 0
66%6 / 05 / 03 / 0
Sum10 / 207 / 353 / 87

Full-sample and conditional denominators answer different questions

The conditional version divides the positive excess sum by n₊ and the squared shortfall sum by n₋. Thus UPR_cond = (sum(uᵢ)/n₊) / sqrt(sum(dᵢ²)/n₋), when both counts are positive. In contrast, the full version includes the frequency of each side: UPR_full = (n₊/n) / sqrt(n₋/n) × UPR_cond. The current PerformanceAnalytics UpsidePotentialRatio function defaults to method="subset"; explicitly request method="full" to match the main calculation here.

  • For t = 0%, using full-sample upside with conditional downside would instead give (10/6)/sqrt(20/2) = 0.527046. This is a third convention; do not silently mix divisors.
  • The full estimator uses each period with equal probability. Conditional averages describe the size of outcomes given that the target was exceeded or missed. A software label alone does not establish which statistic it calculates.
A monthly targetn / n₊ / n₋ / n₀Full UPRConditional UPR
0%6 / 3 / 2 / 10.912871(10/3) / sqrt(20/2) = 1.054093
1%6 / 2 / 3 / 10.483046(7/2) / sqrt(35/3) = 1.024695
3%6 / 1 / 4 / 10.131306(3/1) / sqrt(87/4) = 0.643268

A target change can reverse the ranking

Compare A with another six-month hypothetical series B: -1%, -1%, 1%, 1%, 1%, 2%. Use the same dates, costs and full-sample definition for both. B has more modest gains and smaller shortfalls at a zero target; many of its gains no longer exceed a 1% target.

  • B ranks above A at 0%, while A ranks above B at 1%. Choose the target from the investment objective before inspecting which result looks best. Report a stated target grid when the objective is uncertain.
  • UPR also ignores the order of observations. Reversing either series leaves its ratio unchanged, even though drawdown and time to recovery can change. This is a distribution statistic, not a complete equity-path assessment.
Series and targetPositive sum (pp)Squared shortfall sum (pp²)n₊ / n₋ / n₀UPR
A, 0%10203 / 2 / 10.912871
B, 0%524 / 2 / 01.443376
A, 1%7352 / 3 / 10.483046
B, 1%181 / 2 / 30.144338

UPR is not the Sortino numerator

For the same target and full-sample downside deviation, an unannualized arithmetic Sortino ratio uses mean(r - t) / sqrt(LPM₂). Let LPM₁ = mean(max(t - r, 0)). Then mean(r - t) = UPM₁ - LPM₁, so Sortino = UPR - LPM₁/sqrt(LPM₂). Losses reduce the Sortino numerator as well as entering its denominator.

  • At t = 0%, A has mean excess return (10 - 6)/6 = 0.666667 pp. Its Sortino ratio is 0.365148, versus UPR 0.912871.
  • At t = 1%, A has mean excess return -0.333333 pp. Sortino is -0.138013 while UPR remains 0.483046. A positive UPR does not establish a positive mean excess return.
  • With positive downside deviation, UPR cannot be negative under these definitions. An observed negative value indicates a different numerator or a calculation error.

Run the complete calculation in Python

This standalone Python 3 program uses only the standard library and 50-digit Decimal arithmetic. Supply decimal strings, so 1% is "0.01". It returns the raw moments, full and conditional ratios, counts and an explicit zero-denominator status. The assertions test the hand calculations, target-dependent rankings and invalid inputs. It rejects missing values instead of silently changing n.

Run with python3 upr.py. The example prints all six series/target results followed by PASS. No Pine Script or external package is required.python
from decimal import Decimal, InvalidOperation, getcontext
import json

getcontext().prec = 50
D = Decimal
ZERO = D(0)


def finite_decimal(value):
    if not isinstance(value, (str, Decimal)):
        raise ValueError('Use decimal strings; missing values are not zero.')
    try:
        result = D(value)
    except InvalidOperation as exc:
        raise ValueError('Invalid decimal') from exc
    if not result.is_finite():
        raise ValueError('Returns and target must be finite.')
    return result


def upr(values, target='0'):
    r = [finite_decimal(v) for v in values]
    t = finite_decimal(target)
    if not r:
        raise ValueError('At least one observation is required.')
    excess = [v - t for v in r]
    n = len(r)
    above = sum(v > 0 for v in excess)
    below = sum(v < 0 for v in excess)
    positive_sum = sum((max(v, ZERO) for v in excess), ZERO)
    negative_square_sum = sum((min(v, ZERO) ** 2 for v in excess), ZERO)
    upper = positive_sum / n
    lower = negative_square_sum / n
    downside = lower.sqrt()
    full = upper / downside if downside else None
    conditional = (
        (positive_sum / above) / (negative_square_sum / below).sqrt()
        if above and below else None
    )
    status = ('finite' if downside else
              'no observed shortfall' if upper else 'all equal to target')
    return dict(n=n, above=above, below=below, equal=n-above-below,
                upper=upper, lower=lower, downside=downside,
                full=full, conditional=conditional, status=status,
                sortino=(sum(excess, ZERO) / n / downside
                         if downside else None))


A = ['-0.04', '-0.02', '0', '0.01', '0.03', '0.06']
B = ['-0.01', '-0.01', '0.01', '0.01', '0.01', '0.02']

if __name__ == '__main__':
    results = {f'{name}@{t}': upr(series, t)
               for name, series in [('A', A), ('B', B)]
               for t in ['0', '0.01', '0.03']}
    a = upr(A)
    assert a['upper'] == D('0.10') / 6
    assert a['lower'] == D('0.0020') / 6
    assert abs(a['full'] - D(5) / D(30).sqrt()) < D('1e-48')
    assert upr(A, '0.01')['lower'] == D('0.0035') / 6
    assert upr(A, '0.03')['lower'] == D('0.0087') / 6
    assert upr(B)['full'] > a['full']
    assert upr(A, '0.01')['full'] > upr(B, '0.01')['full']
    assert upr(A[::-1]) == a
    assert upr(A * 2)['full'] == a['full']
    scaled = upr([str(D(v) * 100) for v in A])
    assert abs(scaled['full'] - a['full']) < D('1e-48')
    assert upr(['0', '-0.01'])['full'] == 0
    assert upr(['0', '-0.01'])['conditional'] is None
    assert upr(['0', '0'])['status'] == 'all equal to target'
    assert upr(['0', '0.01'])['status'] == 'no observed shortfall'
    assert upr(['0.01'])['full'] is None
    assert upr(['-0.01'])['full'] == 0
    assert upr(A, '-0.04')['full'] is None
    assert upr(A, '0.06')['full'] == 0
    for bad in [[], ['NaN'], ['Infinity'], [None], [''], [0.01]]:
        try:
            upr(bad)
        except ValueError:
            pass
        else:
            raise AssertionError(f'Expected rejection: {bad}')
    print(json.dumps(results, default=str, indent=2))
    print('PASS: formulas, target ranking, scale, order, repetition, boundaries')

Handle missing observations and zero downside explicitly

A finite number requires a positive downside denominator. The program returns None for a ratio that cannot be computed and preserves the raw components. A one-period input can produce an algebraic result, but cannot describe a reliable return distribution.

  • Do not replace a zero denominator with a small constant to manufacture a finite ranking. A software infinity display does not mean that future downside is impossible.
  • An actual flat period stays in n. Adding an observation exactly equal to t reduces a positive finite full UPR because both sums stay fixed while n grows. Dropping flat periods would change the estimator.
  • When comparing strategies, reconcile dates and use the same complete intervals. Do not discard only the losing or missing months of one strategy. Report exclusions and why those values are unavailable.
Input conditionFull-sample resultConditional result or data action
All observations equal t0/0; undefinedUndefined; both side counts are zero
Some r > t, none r < tNo finite UPR; no observed shortfallUndefined; n₋ = 0
Some r < t, none r > tUPR = 0Undefined; n₊ = 0
A with t = -4%No observed shortfallNo finite ratio
A with t = 6%UPR = 0No upside observations
Empty, blank, NaN or infinite inputReject inputResolve the data gap before computing

Keep frequency, units and sample selection fixed

Report UPR with its observation frequency rather than automatically multiplying it by sqrt(12) or sqrt(252). Partial moments of aggregated returns do not generally scale like an arithmetic mean and a standard deviation. To compare annual targets with monthly returns, choose a monthly target explicitly; an effective annual target T converts to (1 + T)^(1/12) - 1, while T/12 is a different nominal convention.

  • For +1% and -1% monthly returns at t = 0, monthly UPR is 0.707107. Their compounded two-month return is -0.01%, whose one-observation UPR is 0. Scaling the monthly ratio by sqrt(2) would give 1, not 0. The single aggregate observation is only an illustration of the scaling failure.
  • Converting all returns and the target from decimals to percentage points preserves UPR. Increasing leverage while leaving a nonzero target fixed is not that unit conversion, and financing costs can change the return distribution.
  • Currency trade P&L can be used in an explicitly labeled cash-outcome statistic only with a target in the same currency per trade. Trade return percentages and monthly account returns differ when holding periods, sizing, overlapping positions or idle capital differ.
  • For periodic account returns, include open positions at each valuation, consistent costs, cash treatment and external-flow adjustments. A trade-close record cannot recover unrealized month-end changes by itself.
  • Six observations illustrate arithmetic, not statistical reliability. Rare losses may be absent, one large winner may dominate UPM₁, and a near-zero denominator magnifies error. Keep preselected dates and a held-out evaluation period; record all strategy/target trials rather than reporting only the best ratio.

Reconcile TradingView trade data before using the report

TradingView exports report tabs separately. Download List of Trades for trade-level reconciliation. Backtest Deep Report parses trade identifiers, entry/exit type, date and time, price, quantity, net P&L and P&L percentage; it also maps favorable and adverse excursion fields. Those fields describe trades, not an uninterrupted sequence of marked-to-market account returns.

  • The current report exposes net profit, drawdown, Sharpe, Sortino and other supported metrics. Its risk inputs include individual trade P&L percentages. Its monthly heatmap groups net P&L by exit month and divides by running starting equity; it does not supply every unrealized month-end valuation or automatically fill months without exits.
  • Check parsed trades, currency, initial capital and inclusion of open trades against the original export. Supply a separate dated account-return series if your UPR is intended to measure monthly performance.
  • This page calculates UPR independently. It does not claim that Backtest Deep Report includes a built-in UPR metric. Keep the Python output and its target, frequency and missing-data policy beside the supported report results.
  • For a custom implementation, give Pine Script AI Coding Agent the exact equity series, sampling schedule, target units, full-sample divisor and zero-denominator rules. Review and validate the generated script against the matrix above before relying on its output.
Where Pineify fits

Backtest Deep Report

Import your TradingView trade CSV to reconcile supported trade metrics, then calculate the separately defined UPR from an appropriate return series.

Also useful: Pine Script AI Coding Agent. Specify a custom target-return statistic and its sampling rules, then review and test the generated implementation against known values.

Review trade report inputs

Frequently asked questions

Educational information, not investment advice. All return series on this page are hypothetical. Historical UPR, rankings and backtest statistics do not guarantee future returns or capture every source of risk.

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.