TradingView strategy research

Risk-adjusted momentum: define the denominator before ranking

Risk-adjusted momentum ranks past returns relative to a stated risk measure. The denominator can change the winner even when every asset uses the same return history. In the hypothetical example below, raw compounded return selects A, return divided by sample volatility selects B, and return divided by downside deviation selects C. None of these rankings establishes future performance. Specify the numerator, risk estimator, observation dates and invalid-data policy before comparing results.

See the decision guide

Write the score contract before selecting assets

This page defines three teaching scores, not a named index methodology. Let r(i,t) be a monthly simple total return in decimal form, such as 0.04 for 4%. Every candidate uses the same currency, six consecutive completed months and the same cutoff. Distributions and corporate actions must be treated consistently. The invented assets A, B and C avoid any claim that current securities were tested.

  • A population standard deviation divides squared deviations by n. For the same n across assets, it is s × sqrt((n - 1) / n), so every R/s score receives the same positive multiplier and its ranking stays unchanged. Unequal sample lengths remove that invariant; this example rejects incomplete histories.
  • For downside deviation, dividing only by the number k of below-target months produces a conditional shortfall statistic. It is a different denominator. At T = 0, k is 2 for A, 2 for B and 1 for C; the divisors here remain 6 for all three.
QuantityExact definitionInterpretation
Raw momentum RProduct of (1 + r) over L = 6 months, minus 1A compounded holding-period return, not the sum or arithmetic mean of monthly returns.
Monthly sample volatility ssqrt(sum((r - mean(r))²) / (n - 1)), n = 6All six observations enter; denominator is 5. Requires at least two returns.
Monthly downside deviation dsqrt(sum(min(r - T, 0)²) / n), T = 0All six observations enter the divisor, including months with zero shortfall.
Volatility-adjusted scoreR / sSix-month return per unit of monthly sample volatility. A ranking convention with mixed horizons.
Downside-adjusted scoreR / dThe same six-month return divided by monthly target shortfall. No target subtraction in this custom numerator.

Reproduce the three rankings from these monthly returns

The matrix contains hypothetical January through June 2025 total returns. A zero-skip decision uses the June month-end close as its information cutoff and trades no earlier than the next executable session. Each table column below uses exactly this matrix. No prices, costs or later holding returns are assumed.

  • A compounds to 1.15 × 0.94 × 1.08 × 0.97 × 1.10 × 1.02 - 1 = 27.06151832%. Its arithmetic monthly mean is 4.333333%, and its six monthly returns sum to 26%. These are three different quantities.
  • For A, the monthly sample variance is 0.006506666667; its square root is 8.066391%. Downside deviation is sqrt((0.06² + 0.03²) / 6) = 2.738613%. Positive months contribute zero shortfall, but remain in the count.
AssetJanFebMarAprMayJun
A+15%-6%+8%-3%+10%+2%
B+4%-1%+3%+2%-1%+4%
C+6%-0.1%+0.5%+0.5%+0.5%+0.5%

Changing the risk measure changes the selected asset

Sort each score from highest to lowest using unrounded values. Exact ties use the identifier in ascending order. The displayed ratios are unitless numbers, not percentages; both numerator and denominator were calculated from decimal returns. A top-one selector chooses A, B or C depending on which score was frozen before the test.

  • A has the highest cumulative return but the most variable path. C has only one below-zero month, a loss of 0.1%. Its very small estimated downside denominator drives the large ratio. Six observations cannot establish that its future downside risk is small.
  • Using only below-target counts would give downside denominators of 4.743416%, 1% and 0.1%. These differ from the table and scale assets unequally. The ranking happens to remain C, B, A in this matrix; that need not hold for another matrix.
  • The score is a selection rule, not a position size. Equal weighting, volatility targeting, leverage and concentration caps require separate specifications. A high score does not authorize a larger allocation.
AssetR; raw rankSample sR/s; rankDownside dR/d; rank
A27.061518%; 18.066391%3.354848; 32.738613%9.881469; 3
B11.371601%; 22.316607%4.908732; 10.577350%19.696191; 2
C8.027817%; 32.306874%3.479955; 20.040825%196.640557; 1

Change windows and targets as separate experiments

A return window and a risk window need not be equal, but they must be stated independently. Keeping the six-month numerator and estimating risk from only April through June changes R/s to 4.126843 for A and 4.518616 for B. C has three identical +0.5% returns, so its sample volatility is zero and its adjusted score is excluded under the policy below. It does not rank first with an infinite score.

  • No annualization is applied to the example. Multiplying every monthly volatility by sqrt(12) would divide every R/s score by sqrt(12) and preserve its ranking, but would still leave a six-month numerator. It would not create an annual Sharpe ratio.
  • Annualized standard deviation via sqrt(12) relies on an appropriate time-aggregation assumption, including absent serial covariance. Downside deviation of annual compounded returns must be computed against an annual target; multiplying monthly downside deviation by sqrt(12) is not a general identity.
  • Require consecutive observations on an aligned calendar. Do not compress gaps, mix daily risk with monthly risk without a conversion policy, or use a later-close market observation unavailable at the decision cutoff.
SettingConvention and consequence
Risk windowBaseline L = W = 6 monthly returns. Alternative W = 3 keeps R fixed but changes the risk estimate and C eligibility; record both eligible sets.
Downside targetT = 0 per month in the baseline. At T = 1% per month, d becomes 3.291403%, 1.154701% and 0.606905% for A, B and C. These remain R/d custom scores, not Sortino ratios.
Annual target conversionA 6% effective annual hurdle corresponds to (1.06)^(1/12) - 1 per month if constant monthly compounding is intended. It is not a 6% monthly hurdle.
One-month skipFor a decision after June closes, a six-return window with skip = 1 uses December through May and needs a November endpoint. June is omitted. Supply December data; do not silently shorten this six-month matrix to five returns.
Longer conventional windowFrench describes a momentum factor using prior months 2 through 12. That is a specific published convention; it does not make this custom six-month experiment an implementation of that factor.
Execution timingFreeze all observations after the common month-end cutoff, submit later orders, and apply the next executable fill and costs. A close-based signal cannot justify an earlier fill at that same close.

A momentum quotient is not automatically Sharpe or Sortino

William Sharpe defines the historical Sharpe ratio using the arithmetic mean and standard deviation of differential returns. With monthly risk-free returns f(t), form e(t) = r(t) - f(t), then calculate mean(e) / sampleSD(e). If f varies through time, sampleSD(e) need not equal sampleSD(r). The compounded six-month R/s in this page therefore has a different numerator and may have a different denominator.

  • A periodic historical Sortino convention uses (mean(r) - T) / d(T), with the same periodic target and a declared downside estimator. At T = 0, asset A has a monthly Sortino-style quotient of 0.043333333 / 0.027386128 = 1.582310, compared with the cumulative R/d here of 9.881469. The label must follow the formula.
  • An excess momentum numerator could instead subtract a compounded cash return from R, or compare wealth ratios (1 + R) / (1 + Rcash) - 1. Those are different research choices. Neither is the arithmetic mean of monthly excess returns; name the chosen one explicitly.
  • A relative ranking always orders the valid candidates, including negative scores. An absolute filter tests a separate condition, such as R > 0 or R > Rcash. For this optional top-one policy, rank first and accept the winner only if its raw R > 0; otherwise allocate to the separately defined cash account. Equality fails the filter.

Run the complete calculation in Python 3

Copy this standard-library-only script into a .py file and run it with Python 3. It prints the input-derived metrics, all three rankings, the three-month risk-window case and the 1% monthly downside-target case. It also asserts the missing-data, zero-risk, negative-return, skip and tie policies. Dates, data acquisition, trades and portfolio returns are outside this calculation; inputs must already be consecutive monthly returns in chronological order.

Standalone ranking calculation. Teaching data only; no portfolio backtest or Pine strategy orders.python
from math import isfinite, prod, sqrt
from statistics import mean, stdev

DATA = {
    "A": [0.15, -0.06, 0.08, -0.03, 0.10, 0.02],
    "B": [0.04, -0.01, 0.03, 0.02, -0.01, 0.04],
    "C": [0.06, -0.001, 0.005, 0.005, 0.005, 0.005],
}
EPS = 1e-10  # Monthly decimal-return denominator exclusion threshold.


def metrics(history, lookback=6, risk_window=6, skip=0, target=0.0):
    if (not isinstance(lookback, int) or lookback < 1
            or not isinstance(risk_window, int) or risk_window < 2
            or not isinstance(skip, int) or skip < 0
            or not isfinite(target)):
        raise ValueError("Invalid window, skip or target")
    end = len(history) - skip
    required = max(lookback, risk_window)
    if end < required:
        return None
    window = history[end - required:end]
    # Missing, nonfinite or total-loss data is not a usable live candidate.
    if any(x is None or not isfinite(x) or x <= -1 for x in window):
        return None
    returns = history[end - lookback:end]
    risk = history[end - risk_window:end]
    R = prod(1 + x for x in returns) - 1
    s = stdev(risk)
    d = sqrt(sum(min(x - target, 0.0) ** 2 for x in risk) / len(risk))
    if not all(isfinite(x) for x in (R, s, d)):
        return None
    return {"R": R, "s": s, "d": d, "mean": mean(returns),
            "below": sum(x < target for x in risk),
            "vol_score": R / s if s > EPS else None,
            "down_score": R / d if d > EPS else None}


def rank(rows, field):
    valid = [(name, row[field]) for name, row in rows.items()
             if row is not None and row[field] is not None]
    return [name for name, value in sorted(valid, key=lambda x: (-x[1], x[0]))]


def positive_winner(rows, field):
    order = rank(rows, field)
    return order[0] if order and rows[order[0]]["R"] > 0 else "CASH"


rows = {name: metrics(values) for name, values in DATA.items()}
for name, row in rows.items():
    print(name, {key: round(value, 9) for key, value in row.items()})
for field in ("R", "vol_score", "down_score"):
    print(field, rank(rows, field))
assert rank(rows, "R") == ["A", "B", "C"]
assert rank(rows, "vol_score") == ["B", "C", "A"]
assert rank(rows, "down_score") == ["C", "B", "A"]
short = {k: metrics(v, risk_window=3) for k, v in DATA.items()}
print("W=3", short, rank(short, "vol_score"))
print("T=1%", {k: metrics(v, target=0.01) for k, v in DATA.items()})
assert short["C"]["vol_score"] is None
assert metrics([0.01] * 6)["vol_score"] is None
assert metrics([0.01] * 6)["down_score"] is None
assert metrics([0.0] * 6)["R"] == 0
assert metrics([None] + DATA["A"][1:]) is None
assert metrics([float("nan")] + DATA["A"][1:]) is None
assert metrics([-1.0] + DATA["A"][1:]) is None
assert metrics(DATA["A"], skip=1) is None  # Five returns are insufficient.
assert metrics([0.99] + DATA["A"] + [-0.9], skip=1) == rows["A"]
assert metrics([0.01, 0.01 + 1e-12] * 3)["vol_score"] is None
negative = {"N": metrics([-0.02, -0.01] * 3)}
assert rank(negative, "vol_score") == ["N"]
assert positive_winner(negative, "vol_score") == "CASH"
assert rank({"B": rows["A"], "A": rows["A"]}, "R") == ["A", "B"]
print("All policy checks passed")

Keep undefined estimates and bad data visible

The script excludes a candidate from an adjusted ranking when its denominator is at most 1e-10 in monthly decimal-return units. It does not floor the denominator, replace it with 1 or emit infinity. This threshold is a numerical policy, not an economic risk floor; use and disclose a larger minimum-risk rule separately if the strategy requires one.

CaseDefined result
Constant positive returnsRaw R is valid. Volatility and zero-target downside deviation are zero; both adjusted scores are unavailable. No risk-free interpretation follows.
All zero returnsRaw R is zero. Both ratios are unavailable. The strict R > 0 absolute filter fails.
Valid negative cumulative returnCompute signed ratios when denominators are positive; retain them in descending relative rankings. A larger denominator can make a negative quotient less negative, so inspect signs and any absolute filter.
Missing, NaN or infinite observationExclude the candidate for that decision if a required observation is invalid. Do not insert zero or bridge the gap. Report which assets were excluded.
A return of -100% or lowerExclude from this live-candidate ranking. A held asset losing 100% still belongs in a separate portfolio loss ledger; exclusion must never erase a realized loss. Values below -100% are invalid for this unlevered simple-return input.
No eligible adjusted candidatesThe rank list is empty; the optional positive-winner function returns CASH. Existing holdings require an executable exit policy, not an assumed free liquidation.

Move the specification into TradingView without inventing a portfolio test

Use Pine Script AI Coding Agent to draft a finite-symbol observation table with these explicit formulas, confirmed monthly data and visible timestamps. Check the displayed values against the Python fixture before introducing orders. TradingView strategies trade the chart symbol; reading other symbols does not create a broker ledger that trades the ranked universe.

  • A complete multi-asset test needs point-in-time membership, consistent return data, per-asset positions, next-period fills, cash, corporate actions, fees, spread and slippage. The six-month table contains no subsequent returns and proves no investment performance.
  • Strategy Optimizer can compare exposed input settings and TradingView reports for a chart strategy. Keep lookback, risk window, target and skip trials in a dated log; freeze choices before an untouched evaluation period. A chart report does not validate universe-wide allocation returns.
  • Compare alternatives on identical historical universes and execution assumptions. Report exclusions, turnover, drawdown and net costs alongside results; changing a denominator can change both the selected assets and their trading frequency.
Where Pineify fits

Pine Script AI Coding Agent

Draft a finite-symbol ranking observer from the formulas, date rules and invalid-data policies above. Review its output against the reproducible matrix and validate generated Pine source before use.

Also useful: Strategy Optimizer. Compare exposed settings of a chart-symbol strategy using TradingView reports. Multi-asset ranking research still needs separate portfolio accounting.

Draft a momentum ranking observer

Frequently asked questions

Educational research, not investment advice. Assets and returns are hypothetical. No historical performance test, future return or low-risk status is claimed. Estimated risk can miss losses, liquidity problems and changes in market conditions.

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.