D14-F03-A04 / Released engineering topic

Distributionally Robust Portfolio: Optimize a Worst-Loss Event Without Calling a Score a Probability

Minimizes the worst-case probability of a loss across every distribution sharing a given mean and covariance, subject to a return floor.

Distributionally Robust Portfolio: Optimize a Worst-Loss Event Without Calling a Score a ProbabilityD14 / D14-F03

Two return distributions can share the same mean and covariance while assigning very different probability to a painful loss. If we optimize as though one fitted distribution were unquestionably correct, the decimal precision of the weights can hide a much larger modeling uncertainty.

Distributionally robust optimization (DRO) makes that uncertainty explicit. Instead of choosing one return distribution, we declare a class of admissible distributions and optimize the weakest outcome over that class.

This tutorial uses one precise formulation from Bardakci and Lagoa. It defines a radial class around supplied mean and covariance, a loss event, and a return floor. Under the source theorem, the worst-loss probability has the same minimizers as a simple ratio. We will implement that ratio carefully—and never pretend its numeric value is automatically a probability.

Starting toolkit

First understand the uncertainty-set distinction in robust mean–variance. Here a density varies rather than just a parameter. You should be able to state the class, distinguish a floor from an equality, and verify both optimization layers.

What is uncertain here?

Robust mean-variance allowed the mean vector to move inside an ellipsoid while covariance stayed fixed. This topic is different. The uncertain object is the probability density of the return vector.

Write X=mu+Delta. The selected ambiguity class contains zero-mean radial densities of the form

fΔ(d)=g(dTΣ−1d)f_\Delta(d)=g(d^T\Sigma^{-1}d)

where g is non-negative, non-increasing, and zero beyond a common bounded- support parameter in the paper. The covariance is positive definite so the radial metric is defined.

That is a particular class, not a synonym for every DRO model. Wasserstein balls, moment-confidence sets, divergence balls, robust CVaR, and distributionally robust Kelly are neighboring formulations with different assumptions and results.

Define the loss before minimizing it

Let alpha>0 be the loss magnitude. For portfolio w, the adverse event is

XTw≤−αX^Tw\leq-\alpha

The distributionally robust risk is

Riskα(w)=sup⁡f∈FPr⁡f(XTw≤−α)\mathrm{Risk}_\alpha(w)=\sup_{f\in\mathcal F}\Pr_f(X^Tw\leq-\alpha)

We choose long-only weights summing to one and require expected return mu^T w>=gamma. Under the source class and theorem, a loss-minimizing portfolio can be found by minimizing

wTΣwα+μTw\frac{\sqrt{w^T\Sigma w}}{\alpha+\mu^Tw}

From a distribution class and loss event to the ratio search

The numerator is a dispersion scale under the supplied shape matrix Sigma. The denominator adds the loss threshold and expected return. A lower ratio receives the lower worst-case loss ranking under the theorem.

Here is the boundary I want visible on every screen: the ratio is called risk_score. A score of 1.57 is not a 157% probability. The theorem gives an equivalent minimizer under its class; it does not say the raw ratio is a universal probability measure.

Why the solution becomes manageable

Fix beta=mu^T w. At that one expected return, the denominator is fixed, so we only need the portfolio with minimum variance:

min⁡w≥0,  1Tw=1,  μTw=βwTΣw\min_{w\geq0,\;\mathbf1^Tw=1,\;\mu^Tw=\beta}w^T\Sigma w

subject to the long-only simplex and mu^T w=beta. This is a convex quadratic frontier problem. We solve it for candidate beta values between the feasible floor and the maximum asset mean, evaluate the ratio, and perform a bounded one-dimensional search.

A QP solver can certify one inner point. It cannot by itself certify that the outer search is complete. Production code should retain both levels of status.

A synthetic example with a binding floor

Use:

Plain text
mu = [0.06, 0.03]
Sigma = [[0.04, 0.00],
         [0.00, 0.01]]
alpha = 0.02
gamma = 0.04

At equal weights, expected return is 0.045, variance is 0.0125, and

risk_score=sqrt(0.0125)/(0.02+0.045)=1.72005229.

Asset B alone has lower variance, but its expected return is 0.03, below the floor. Writing w=[x,1-x], the floor requires x>=1/3. The unconstrained ratio minimum occurs below that boundary, so the constrained optimum sits exactly at x=1/3.

The result is:

Plain text
weights = [0.3333333333, 0.6666666667]
expected return = 0.04
variance = 0.0088888889
risk_score = 1.5713484026

Every value is author-derived from synthetic moments. No observed asset or future performance is implied.

Input validation cannot be delegated to DRO

The optimizer consumes mean and covariance. Those moments can still be wrong because a split was handled inconsistently, a cash dividend disappeared from wealth, a price belonged to the wrong date or instrument, or quantities from old holdings changed unit. Multi-currency portfolios add another layer: every return must share a declared reporting currency and FX timestamp.

I treat those checks as hard gates. Distributional ambiguity is not a bucket for arbitrary data corruption. If a return row is economically impossible, reject or repair it through a governed source process before estimating the moments. Do not enlarge an ambiguity set to hide a broken security master.

Freeze the ambiguity class precisely

For a fixed positive-definite shape matrix Σ\Sigma and fixed finite ymax>0y_{max}>0, use normalized densities

fΔ(d)=g(dTΣ−1d),∫RnfΔ(d) dd=1,f_\Delta(d)=g(d^T\Sigma^{-1}d),\qquad \int_{\mathbb R^n}f_\Delta(d)\,dd=1,

with g≥0g\geq0, gg non-increasing, and g(y)=0g(y)=0 for y>ymaxy>y_{max}. Returns are X=μ+ΔX=\mu+\Delta. Symmetry gives zero mean disturbance. These are the conditions in Bardakci–Lagoa, §II-A, equations (3)–(7) and Lemma 1. The API’s historical name covariance supplies this shape; the class does not fix every member’s covariance to that matrix.

Why the support bound is absent from the optimizer

Here is a geometric derivation. Under the largest uniform ellipsoid, transform Δ=Σ1/2Z\Delta=\Sigma^{1/2}Z, where ZZ is uniform on the ball of radius ymax\sqrt{y_{max}}. Rotational symmetry reduces the loss event to a cap whose normalized cutoff is

h=α+μTwymaxwTΣw=1ymax risk_score(w).h=\frac{\alpha+\mu^Tw}{\sqrt{y_{max}}\sqrt{w^T\Sigma w}} =\frac{1}{\sqrt{y_{max}}\,\mathrm{risk\_score}(w)}.

For a positive denominator, smaller score means a smaller adverse cap. The fixed support bound changes the cap probability, but its common scale cannot reverse the ranking of portfolios. When h≥1h\geq1, the cap is empty: several portfolios may have zero loss probability. The ratio selects a minimizer; do not promise a unique probability-optimal portfolio or identical optimizer sets on a flat zero-probability region.

Also, the uniform ellipsoid has covariance ymaxΣ/(n+2)y_{max}\Sigma/(n+2), not generally Σ\Sigma. Treating this as a moment-equality ambiguity class would silently change the model. We return a ranking score and do not calibrate ymaxy_{max} or calculate the cap probability. This limitation is visible beside every output.

The floor need not bind

In the two-asset fixture, write w=[x,1−x]w=[x,1-x]. Then

s2(x)=0.05x2−0.02x+0.01,α+μTw=α+0.03+0.03x.s^2(x)=0.05x^2-0.02x+0.01,\qquad \alpha+\mu^Tw=\alpha+0.03+0.03x.

Differentiating the squared ratio gives one linear equation after cancellation. With α=0.02\alpha=0.02, the unconstrained optimum is x=2/7x=2/7. At floor 3%, it remains feasible: return is 3.857142857% and score is 1.5617376189. Imposing return exactly 3% instead would wrongly choose all B with score 2.

At floor 4%, the floor binds and x=1/3x=1/3. At floor 6%, only the highest-mean face is feasible: [1,0]. If several assets tie for the maximum mean, minimize variance across that tied face; do not silently substitute the unrestricted GMV portfolio.

Read both certificates

Inner fixed-return problems use scaled KKT equations and check nonnegativity, budget, target, and stationarity residuals. The reference enforces at most 12 assets because face enumeration is exponential. The outer golden-section search operates on the positive-denominator ratio, whose sublevel sets are convex; the scalar frontier value is quasiconvex. Both endpoints are evaluated.

The output adds search_bracket and return_residual (camelCase in TypeScript). Status optimal means the final return bracket meets the requested absolute return tolerance and the inner solves succeeded. It is a numerical certificate, not a probability guarantee. With one outer iteration, the free-floor case must return nonconverged.

Try it: increase α\alpha with the floor at 3%. The score falls for any fixed portfolio, but the optimal weights can change as well. Recomputing only the denominator at old weights is not a fresh optimization.

Implementation walkthrough

The Python reference and TypeScript reference enumerate active asset subsets for each fixed-return inner problem and then use the same golden-section search across the feasible return interval. This small- universe design is readable and deterministic.

The function rejects non-positive alpha, an infeasible gamma, indefinite shape matrix, unresolved KKT system, and a non-positive denominator. Its output carries an interpretation string so a consumer does not silently relabel the score. For larger universes, replace the reference solver with a mature QP and a documented scalar-search wrapper; do not change the contract.

Use the lab as a model review

Open the distributionally robust portfolio playground. Step through the class, loss event, feasible return interval, fixed-return frontier, and final score. Raise gamma and watch the feasible set contract. Raise alpha and observe the denominator change. The failure scenario puts the floor above every asset mean and stops before any solver result appears.

The lab never simulates colorful paths and calls the picture DRO. Monte Carlo can illustrate distributions, but here the mathematical result is defined by the supremum over F and the theorem's equivalent optimization.

Tests and claims

Tests reproduce the equal-weight score and the binding-floor optimum, verify the return constraint, tighten the floor, reject an impossible floor, reject a zero threshold, reject indefinite covariance, and compare Python with TypeScript. The fixed fixture lets an independent reviewer solve the scalar boundary by hand.

The historical-example decision is deferred. A named market case needs a licensed point-in-time panel, complete adjustment and currency basis, and evidence that the radial class is an appropriate modeling choice. The primary paper's academic examples support the method; they are not republished here as market performance evidence.

What this result does not mean

It does not mean the future distribution lies in F. It does not calibrate the bounded-support parameter, estimate mu or Sigma, or model costs, liquidity, taxes, leverage, or path-dependent drawdown. It does not prove a lower score will earn more money. And it does not let us transfer Wasserstein or moment-set guarantees into this radial model.

Distributional robustness is valuable because the ambiguity is explicit. It becomes misleading the moment the class, loss event, or theorem boundary is hidden behind one “risk” number.

Run the calculation yourself

From this topic folder, use Python 3.10 or later. No third-party numerical library is required:

Shell
python examples/python_example.py
python -m unittest discover -s tests -p "test_*.py"

The key call is shown below. For an interactive Python session, first make the local implementation importable:

Python
import sys
sys.path.insert(0, "implementations/python")
from distributionally_robust_portfolio import distributionally_robust_portfolio
print(distributionally_robust_portfolio([.06,.03],[[.04,0],[0,.01]],.02,.04).to_dict())

The TypeScript example uses the same inputs. See the runnable commands for compilation and execution. Shared cases in the shared regression cases exercise boundary states in both languages; strings NaN and Infinity are test sentinels, not valid numerical inputs.

Summary

An auditable DRO portfolio begins with four visible objects: an ambiguity class, a loss event, a feasible holdings set, and a result whose interpretation matches the theorem. For the selected radial class, fixed-return variance problems plus a scalar search provide a transparent implementation. The score ranks worst-loss probability under the source conditions; it is not itself a universal probability.

Next, Kelly Allocation keeps supplied scenario probabilities fixed but changes the objective to expected logarithmic wealth.

Choose the family method by the decision you face

Your real questionStart withWhat changesWhat it does not solve
How should uncertain views modify a market prior?Black-LittermanExpected returns through a prior and viewsDistributional ambiguity
How unstable are optimized weights across plausible samples?Resampled Efficient FrontierRows, replicate moments, and rank-matched frontiersA worst-distribution objective
How should a bounded error in the mean affect allocation?Robust Mean-VarianceMean vector inside a declared ellipsoidAmbiguity about the whole return law
Which portfolio ranks best under a declared distribution class and loss event?Distributionally Robust PortfolioThe admissible return distributionUniversal safety or a probability label for the ratio
Which scenario mix maximizes long-run log growth?Kelly AllocationObjective and scenario wealth factorsProtection against an unspecified distribution class

Related concepts and learning handoff

The reviewed The Fintech Builder chapter KKT checks and limits of an optimality claim (17:33–18:43) supports review of each fixed-return inner problem. It does not establish the radial-class theorem, turn the ratio into a probability, or transfer guarantees from another ambiguity set.

distributionally robust flow

ReferencesPrimary sources and evidence notes

Expand the source trail, evidence role, and limitations behind the engineering choices.

BL2019 — Distributionally Robust Portfolio Optimization

  • Organization or authors: I. E. Bardakci and C. M. Lagoa.
  • Source type: Original peer-reviewed IEEE conference paper; NIH/PMC author manuscript.
  • Publication or effective date: December 2019, CDC 2019, pages 1526–1531.
  • Version: DOI 10.1109/CDC40024.2019.9029381; PMC7956065.
  • URL or DOI: NIH/PMC author manuscript, Penn State record.
  • Accessed: 2026-09-16.
  • Jurisdiction: None; mathematical portfolio methodology.
  • Supports: Radial admissible density class, equations (2)–(5), worst-case loss probability, equivalent ratio minimization, and QP plus one-dimensional search.
  • Limitations: The equivalence is conditional on the paper's class and theorem; the ratio is not an unconditional probability.

DY2010 — Distributionally Robust Optimization Under Moment Uncertainty

  • Organization or authors: Erick Delage and Yinyu Ye.
  • Source type: Original peer-reviewed Operations Research article.
  • Publication or effective date: 2010, 58(3), 595–612.
  • Version: DOI 10.1287/opre.1090.0741.
  • URL or DOI: INFORMS record.
  • Accessed: 2026-09-15.
  • Jurisdiction: None.
  • Supports: Moment-confidence-region DRO as a major nearby formulation with a portfolio application.
  • Limitations: It is not silently substituted for the radial class.

EK2018 — Data-driven DRO using the Wasserstein metric

  • Organization or authors: Peyman Mohajerin Esfahani and Daniel Kuhn.
  • Source type: Original peer-reviewed Mathematical Programming article and author manuscript.
  • Publication or effective date: 2018, 171, 115–166.
  • Version: DOI 10.1007/s10107-017-1172-1.
  • URL or DOI: Author manuscript.
  • Accessed: 2026-09-15.
  • Jurisdiction: None.
  • Supports: Wasserstein-ball DRO as a distinct data-driven alternative.
  • Limitations: Its guarantees and reformulations do not apply automatically to this package's radial ambiguity class.

Equation review note

The indexed PMC full text was inspected on 2026-09-16, including §II-A, theorem 1 and Lemma 1. The exact class and normalization are written in the definition contract and article. The cap geometry and covariance scaling in the article are author derivations. A flat zero-loss cap can admit probability ties; only selection of a minimizer is claimed. Direct PMC navigation may show an access challenge even when its indexed text is available.

distributionally-robust-portfolio.ts
export class ContractError extends Error {
}
;
type V = number[];
type M = number[][];
const dot = (a: V, b: V) => a.reduce((s, x, i) => s + x * b[i], 0), mv = (a: M, x: V) => a.map(r => dot(r, x));
function* combos(n: number, k: number, s = 0, p: number[] = []): Generator<number[]> { if (p.length === k) {
    yield p;
    return;
} for (let i = s; i < n; i++)
    yield* combos(n, k, i + 1, [...p, i]); }
function spd(s: M) { const n = s.length; if (s.some((r, i) => r.some((x, j) => Math.abs(x - s[j][i]) > 1e-12)))
    throw new ContractError("covariance must be symmetric"); const l = Array.from({ length: n }, () => Array(n).fill(0)); for (let i = 0; i < n; i++)
    for (let j = 0; j <= i; j++) {
        const z = s[i][j] - Array.from({ length: j }, (_, k) => l[i][k] * l[j][k]).reduce((a, b) => a + b, 0);
        if (i === j) {
            if (z <= 1e-14)
                throw new ContractError("covariance must be positive definite");
            l[i][j] = Math.sqrt(z);
        }
        else
            l[i][j] = z / l[j][j];
    } }
function consistentSolve(a: number[][], b: number[]): number[] {
    const g = a.map((row, i) => { const s = Math.max(...row.map(Math.abs), Math.abs(b[i]), 1e-300); return [...row.map(x => x / s), b[i] / s]; });
    const columns = a[0].length, pivots: number[] = [];
    let pr = 0;
    for (let col = 0; col < columns && pr < g.length; col++) {
        let p = pr;
        for (let r = pr + 1; r < g.length; r++)
            if (Math.abs(g[r][col]) > Math.abs(g[p][col]))
                p = r;
        if (Math.abs(g[p][col]) < 1e-12)
            continue;
        [g[pr], g[p]] = [g[p], g[pr]];
        const z = g[pr][col];
        g[pr] = g[pr].map(x => x / z);
        for (let r = 0; r < g.length; r++)
            if (r !== pr) {
                const f = g[r][col];
                g[r] = g[r].map((x, j) => x - f * g[pr][j]);
            }
        pivots.push(col);
        pr++;
    }
    if (g.some(row => Math.max(...row.slice(0, -1).map(Math.abs)) < 1e-10 && Math.abs(row[columns]) > 1e-9))
        throw new ContractError("inconsistent KKT system");
    const x = Array(columns).fill(0);
    pivots.forEach((col, r) => x[col] = g[r][columns]);
    return x;
}
/** PSD face enumeration with scaled, possibly redundant equality constraints. */
function minVariance(mu: number[], sigma: number[][], target: number | null): number[] {
    const n = mu.length, scale = Math.max(...sigma.flat().map(Math.abs), 1e-300);
    let best: {
        v: number;
        w: number[];
    } | undefined;
    for (let size = 1; size <= n; size++)
        for (const active of combos(n, size)) {
            const constraints = [Array(size).fill(1)], rhs = [1];
            if (target !== null) {
                const spread = Math.max(...active.map(i => Math.abs(mu[i] - target)));
                if (spread > 1e-14) {
                    constraints.push(active.map(i => (mu[i] - target) / spread));
                    rhs.push(0);
                }
            }
            const k = constraints.length, a = active.map((i, pos) => [...active.map(j => sigma[i][j] / scale), ...constraints.map(c => c[pos])]);
            a.push(...constraints.map(c => [...c, ...Array(k).fill(0)]));
            const b = [...Array(size).fill(0), ...rhs];
            let z: number[];
            try {
                z = consistentSolve(a, b);
            }
            catch {
                continue;
            }
            if (z.some(x => !Number.isFinite(x)) || Math.min(...z.slice(0, size)) < -1e-10)
                continue;
            if (Math.max(...a.map((row, i) => Math.abs(dot(row, z) - b[i]))) > 1e-8)
                continue;
            const w = Array(n).fill(0);
            active.forEach((i, pos) => w[i] = Math.max(0, z[pos]));
            if (Math.abs(w.reduce((s, x) => s + x, 0) - 1) > 1e-9 || (target !== null && Math.abs(dot(mu, w) - target) > 1e-10))
                continue;
            const v = dot(w, mv(sigma, w));
            if (!best || v < best.v - 1e-14 * scale)
                best = { v, w };
        }
    if (!best)
        throw new ContractError("frontier target infeasible or numerically unresolved");
    return best.w;
}
export interface DroResult {
    weights: V;
    expectedReturn: number;
    variance: number;
    riskScore: number;
    lossThreshold: number;
    returnFloor: number;
    outerIterations: number;
    status: "optimal" | "nonconverged";
    interpretation: string;
    searchBracket: V;
    returnResidual: number;
}
export function distributionallyRobustPortfolio(mean: V, covariance: M, lossThreshold: number, returnFloor: number, tolerance = 1e-11, maxIterations = 160): DroResult { const mu = mean.map(Number), n = mu.length, sigma = covariance.map(r => r.map(Number)); if (n > 12)
    throw new ContractError("reference solver supports at most 12 assets"); if (!n || mu.some(x => !Number.isFinite(x)))
    throw new ContractError("mean must be finite"); if (sigma.length !== n || sigma.some(r => r.length !== n) || sigma.flat().some(x => !Number.isFinite(x)))
    throw new ContractError("covariance shape/values invalid"); spd(sigma); if (!Number.isFinite(lossThreshold) || !(lossThreshold > 0))
    throw new ContractError("lossThreshold alpha must be positive"); if (!Number.isFinite(returnFloor) || returnFloor > Math.max(...mu) + 1e-12)
    throw new ContractError("return floor is infeasible"); const lo = Math.max(returnFloor, Math.min(...mu)), hi = Math.max(...mu); if (lossThreshold + lo <= 0)
    throw new ContractError("alpha + feasible expected return must be positive"); if (!Number.isFinite(tolerance) || !(tolerance > 0) || !Number.isInteger(maxIterations) || maxIterations < 1)
    throw new ContractError("outer search controls invalid"); const evaluate = (beta: number) => { const w = minVariance(mu, sigma, beta), variance = dot(w, mv(sigma, w)); return { score: Math.sqrt(Math.max(variance, 0)) / (lossThreshold + dot(mu, w)), w, variance }; }; let winner, iterations = 0, a = lo, b = hi; if (hi - lo <= tolerance)
    winner = evaluate(lo);
else {
    const gr = (Math.sqrt(5) - 1) / 2;
    let c = b - gr * (b - a), d = a + gr * (b - a), fc = evaluate(c), fd = evaluate(d);
    while (b - a > tolerance && iterations < maxIterations) {
        if (fc.score <= fd.score) {
            b = d;
            d = c;
            fd = fc;
            c = b - gr * (b - a);
            fc = evaluate(c);
        }
        else {
            a = c;
            c = d;
            fc = fd;
            d = a + gr * (b - a);
            fd = evaluate(d);
        }
        iterations++;
    }
    winner = [evaluate(lo), evaluate(hi), fc, fd].sort((x, y) => x.score - y.score)[0];
} return { weights: winner.w, expectedReturn: dot(mu, winner.w), variance: winner.variance, riskScore: winner.score, lossThreshold, returnFloor, outerIterations: iterations, status: b - a <= tolerance ? "optimal" : "nonconverged", searchBracket: [a, b], returnResidual: Math.max(0, returnFloor - dot(mu, winner.w)), interpretation: "riskScore ranks worst-case loss probability only under the cited radial-class theorem; it is not itself an unconditional probability" }; }
Full-height labplaygroundOpen full screen
Written by

Fintech engineer building market-data and financial systems, and the author of every article, glossary record, and reference implementation on The Fintech Builder.