TradingView Backtesting

Risk Contributions: Marginal vs Component Portfolio Risk

Risk contribution assigns portfolio volatility to its holdings using their weights and covariances. For volatility σ = √(wᵀΣw), marginal risk contribution is MRC = Σw / σ and component risk contribution is CRCᵢ = wᵢ × MRCᵢ. Components sum to σ; their shares sum to 100%. A negative component means that exposure offsets volatility in this model at the current weights. It does not mean that the asset has negative volatility or cannot lose money.

See the decision guide

Keep the risk measure and units explicit

Use a column vector w of exposures relative to the same positive equity base and a symmetric asset-return covariance matrix Σ. Returns enter as decimals: 1% is 0.01. The matrix diagonal holds variances; the off-diagonal entries hold covariances. Set q = Σw and v = wᵀq. At positive portfolio variance, σ = √v. The Euler decomposition in Palomar gives the volatility components below.

  • Variance is homogeneous of degree two. Its additive allocation uses half the weight times its derivative. Volatility is homogeneous of degree one, so its component uses the full weight times its derivative.
  • A CRC of 0.02 is 2 percentage points of volatility. If total volatility is 0.10, its risk share is 20%. Those two displays answer different questions.
  • For σ > 0, VCᵢ / v and CRCᵢ / σ give the same relative shares. Variance amounts and volatility amounts still have different units.
QuantityFormulaUnits and sum
Marginal volatility contributionMRCᵢ = qᵢ / σDecimal volatility per unit of weight; no additive total
Component volatility contributionCRCᵢ = wᵢqᵢ / σDecimal volatility; sum(CRC) = σ
Percentage risk contribution100 × CRCᵢ / σShare of portfolio volatility; sum = 100%
Variance contributionVCᵢ = wᵢqᵢSquared decimal return; sum(VC) = v
Marginal variance sensitivity∂v / ∂wᵢ = 2qᵢsum(wᵢ × ∂v/∂wᵢ) = 2v, not v

Reproduce a negative hedge contribution

Assume annual asset volatilities of 20% for A and 10% for B, correlation -0.30, and weights [0.80, 0.20]. These are invented model inputs, not market estimates. The annual covariance matrix is [[0.0400, -0.0060], [-0.0060, 0.0100]]. Its determinant is 0.000364 and its eigenvalues are approximately 0.0088445 and 0.0411555, so it is positive definite and therefore positive semidefinite (PSD).

  • Compute v = 0.80 × 0.0308 + 0.20 × (-0.0028) = 0.02408, then σ = √0.02408 = 0.1551773179. The displayed component percentages are percentage points of annual return volatility.
  • The variance components are 0.02464 and -0.00056. They sum to 0.02408. Dividing each by 0.1551773179 yields the volatility components in the table.
  • B has positive standalone volatility but negative covariance with this portfolio. Its positive weight therefore has a negative CRC. A contributes more than 100% because B offsets part of that modeled risk; do not clip either share to the 0% to 100% interval.
  • At weights [0.60, 0.40], the same matrix gives σ = 11.454257%, with components 11.314571 and +0.139686 percentage points. B now has positive contribution. A hedge label is conditional on the complete allocation and covariance estimate.
AssetWeightq = ΣwMRCCRC in volatility pointsRisk share
A0.800.03080.19848262+15.878609%+102.325581%
B0.20-0.0028-0.01804387-0.360877%-2.325581%
Portfolio1.00wᵀq = 0.02408Not summed15.517732%100.000000%

A marginal derivative is not a funded rebalance

MRCᵢ is the unconstrained partial derivative: vary wᵢ while holding all other exposures and Σ fixed. For a small change Δw, Δσ ≈ MRCᵀΔw. Zivot uses this gradient to assess portfolio changes. A fully invested rebalance must specify where its funding comes from.

  • Moving δ from A into B has direction [-1, +1] and sensitivity MRC_B - MRC_A. In the 80% / 20% example this is -0.21652649 per unit weight. A one-percentage-point transfer (δ = 0.01) predicts a -0.216526 percentage-point volatility change.
  • Recalculate the finite move: [0.79, 0.21] has variance 0.0234142 and volatility approximately 15.301699%, a change of -0.216033 percentage points. The difference from the derivative estimate is curvature, not a reconciliation error.
  • Adding B while leaving A fixed describes an exposure increase, or funding from a zero-volatility cash position if the budget includes one. Renormalizing all weights afterward is a different direction and has a different derivative.
  • Removing a holding is a finite change. Its CRC is an Euler allocation at the current point, not the exact difference between volatility before and after liquidation. Recompute both portfolios to measure removal risk.

Run the covariance worksheet

Save the Python 3 code below as risk_contribution.py, install NumPy with python3 -m pip install numpy, then run python3 risk_contribution.py. It accepts an already aligned covariance matrix and weights; it neither retrieves market data nor infers a covariance matrix from trade fills. Set periods_per_year=1 for the annual matrix used here. Outputs are decimal volatility, component amounts, percentage shares and the annual covariance used.

The script checks analytic marginal contributions against central differences and prints the reconciliation. Tiny negative eigenvalues within the declared relative tolerance are clipped and reported; materially non-PSD matrices fail.python
import numpy as np


def risk_contributions(weights, covariance, periods_per_year=1.0):
    w = np.asarray(weights, dtype=float)
    s = np.asarray(covariance, dtype=float)
    p = float(periods_per_year)
    if w.ndim != 1 or w.size == 0 or s.shape != (w.size, w.size):
        raise ValueError("Expected n weights and an n by n covariance")
    if not (np.isfinite(w).all() and np.isfinite(s).all()
            and np.isfinite(p) and p > 0):
        raise ValueError("Missing, infinite or invalid input")
    scale = float(np.max(np.abs(s)))
    if scale == 0:
        raise ValueError("Zero portfolio volatility: contributions undefined")
    tol = 1e-10 * scale
    if np.max(np.abs(s - s.T)) > tol:
        raise ValueError("Covariance must be symmetric")
    # Accept only roundoff-sized asymmetry and negative eigenvalues.
    s = (s + s.T) / 2
    eigenvalues, vectors = np.linalg.eigh(s)
    if eigenvalues[0] < -tol:
        raise ValueError("Covariance is not positive semidefinite")
    repaired = bool(np.any(eigenvalues < 0))
    if repaired:
        s = (vectors * np.maximum(eigenvalues, 0)) @ vectors.T
    with np.errstate(over="raise", invalid="raise", divide="raise"):
        s = s * p
        q = s @ w
        variance = float(w @ q)
        numerical_floor = 1e-12 * float(np.linalg.norm(s, 2)) * float(w @ w)
        if variance <= numerical_floor:
            raise ValueError("Zero/near-zero volatility: attribution unstable")
        sigma = np.sqrt(variance)
        mrc = q / sigma
        crc = w * mrc
        share = 100 * crc / sigma
        if not all(np.isfinite(x).all() for x in (s, mrc, crc, share)):
            raise ValueError("Non-finite calculation")
    if not np.isclose(crc.sum(), sigma, rtol=1e-9, atol=0):
        raise ArithmeticError("Euler reconciliation failed")
    return sigma, mrc, crc, share, s, repaired


S = np.array([[0.04, -0.006], [-0.006, 0.01]])
for weights in ([0.8, 0.2], [0.6, 0.4], [1.2, -0.2]):
    sigma, mrc, crc, share, used, repaired = risk_contributions(weights, S)
    w = np.array(weights)
    # Independent central differences of volatility, without renormalizing.
    h = 1e-6
    directions = np.eye(len(w))
    def vol(x):
        return np.sqrt(x @ used @ x)
    fd = np.array([(vol(w + h*d) - vol(w - h*d)) / (2*h)
                   for d in directions])
    assert np.allclose(fd, mrc, rtol=1e-7, atol=1e-9)
    assert np.isclose(crc.sum(), sigma)
    assert np.isclose(share.sum(), 100)
    print(weights, "sigma", round(sigma, 10))
    print("CRC", np.round(crc, 10), "share %", np.round(share, 6))
    print("max derivative error", np.max(np.abs(fd - mrc)),
          "roundoff PSD repair", repaired)

assert risk_contributions([0.8, 0.2], S)[2][1] < 0
assert risk_contributions([0.6, 0.4], S)[2][1] > 0
# Singular PSD is valid when this allocation has positive variance.
assert np.isclose(risk_contributions([0.5, 0.5], [[.04, .04], [.04, .04]])[0], .2)
# Cash contributes zero in this model, even though the matrix is singular.
assert risk_contributions([0.8, 0.2], [[.04, 0], [0, 0]])[2][1] == 0

Prepare synchronized returns and choose the horizon

A covariance estimate requires asset returns over identical intervals in one reporting currency. Record the observation window, timestamps, valuation convention, income treatment and missing-data policy. For T complete observations of n assets, put observations in rows and assets in columns of R. The sample estimate is Σ = (R - mean(R))ᵀ(R - mean(R)) / (T - 1), or np.cov(R, rowvar=False, ddof=1). At least two complete observations are required; that minimum does not make an estimate reliable.

  • Use simple asset returns for the exact one-period linear relation r_portfolio = wᵀr at fixed starting weights. A weighted sum of asset log returns is generally not the portfolio log return. Time-varying weights need period-specific attribution or a stated current-weight risk estimate.
  • Missing covariance entries remain unknown. Do not replace them with zero: zero encodes an assumption of no co-movement. Pairwise deletion can assemble mutually inconsistent entries and produce a non-PSD matrix; use a documented estimation method and check the complete result.
  • A daily covariance matrix becomes an annualized matrix by Σ_annual = mΣ_daily only under the chosen stable-variance, zero-serial-covariance model. Use an explicit m, such as a 252-session convention, once. Annual volatility, MRC and CRC scale by √m; relative shares stay unchanged.
  • Serial dependence adds cross-time covariance terms. Square-root annualization is then a convention rather than an exact horizon forecast. Do not annualize an already annual matrix again or mix daily variances with annual off-diagonal entries.
  • Estimate on data available at the decision date. Report sensitivity to the window and correlation assumptions; a clean Euler sum verifies arithmetic, not forecasting accuracy.

Handle shorts, cash and invalid matrices explicitly

The formula does not require positive weights or weights summing to one. It requires a consistent exposure base, a valid covariance model and positive portfolio volatility. Keep gross exposure sum(|w|), net exposure sum(w), and financing assumptions with the output. Never silently normalize a leveraged or market-neutral allocation to sum to one.

  • The worksheet accepts relative roundoff asymmetry up to 1e-10 of the largest matrix entry, averages it, and clips negative eigenvalues only within that tolerance. It reports clipping. This numerical convention is not a statistical covariance repair; keep the original matrix for audit.
  • For a net-zero portfolio, weights still refer to a positive capital base. Dividing by net exposure would be undefined. Nonlinear derivatives require a suitable factor or scenario model; these linear asset-return weights do not capture gamma, liquidity or margin calls.
  • Near a perfect hedge, very large positive and negative percentage shares can cancel to 100%. Examine amounts, estimation error and stress scenarios before interpreting the ratios.
CaseRequired interpretation or action
Short positionUse a negative weight and the return of the underlying long asset. Do not negate both the weight and its return series. The sign of CRC depends on both wᵢ and qᵢ.
120% long A, 20% short BThe same example matrix gives 24.673873% volatility and positive CRCs of 23.928145 and 0.745728 points. A short can add risk.
CashA deterministic cash return has zero covariance and zero CRC in this model. Currency risk, variable funding and credit risk require modeled exposures if relevant.
Singular but PSDNo matrix inverse is needed. Duplicate assets or a zero-variance cash row can be valid if wᵀΣw > 0. Individual attribution remains model-dependent.
Zero or numerically near-zero portfolio volatilityMRC and percentage shares involve division by zero or an unstable denominator. Return undefined rather than invented zero shares.
Non-PSD covarianceReject it even when the current weights happen to give positive variance. Another direction can have negative modeled variance. Re-estimate or explicitly document a repair.

Keep attribution separate from risk-parity optimization

This calculation answers which exposures contribute to the volatility of supplied weights. A risk-parity solver searches for weights that satisfy specified risk budgets, along with funding, sign, leverage and other constraints. Equal capital weights do not generally produce equal risk contributions. The worksheet intentionally leaves the supplied weights unchanged.

  • A negative contribution is diagnostic evidence of an offset under this covariance estimate. It does not by itself prescribe increasing the position, and an equal-risk target does not establish a suitable portfolio.
  • Volatility treats positive and negative deviations symmetrically. Review tail losses, drawdown, liquidity, financing and concentration separately. The decomposition is not an expected-shortfall allocation or a maximum-loss guarantee.

Use the trade report for the data it actually contains

TradingView exports report tabs separately; a List of Trades export contains trade information. Pineify Backtest Deep Report parses entry and exit times, prices, quantity, P&L and excursions. Those records are not synchronized return observations for every asset and do not supply this covariance matrix.

  • Use the report to review the imported strategy history and supported metrics. Keep a separate dated returns table, covariance estimate, exposure vector and worksheet output for portfolio attribution. This page does not claim that risk contribution is built into Backtest Deep Report.
  • Pine Script AI Coding Agent can help draft or review strategy code with explicit symbols, timeframes and exposure rules. Check generated code and its observations against an independent calculation. A trade CSV upload does not automatically create the aligned asset histories this method needs.
Where Pineify fits

Backtest Deep Report

Review supported metrics from imported TradingView trade records. Retain the separate return matrix and covariance worksheet for portfolio attribution.

Also useful: Pine Script AI Coding Agent. Draft or review strategy logic with explicit symbols, return periods and exposure assumptions, then verify its output independently.

Review the trade report workflow

Frequently asked questions

Educational information, not investment advice. Examples use hypothetical covariance assumptions. Historical or modeled risk contributions do not guarantee future diversification, losses or returns.

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.