I do not trust a tail-risk number simply because an optimizer returned it. I want to see which scenarios entered the tail, how much probability mass was counted, which sign convention turned returns into losses, and whether the displayed objective can be rebuilt from those pieces.
That discipline matters because portfolio data can be numerically tidy and economically false. A split-adjusted price paired with an old share count can invent a loss. A reversed FX quote can magnify one. A cash dividend can look like a price loss when total return was intended. Mean-CVaR optimization does not repair those errors; it can give them more influence because it deliberately concentrates on the bad tail.
This tutorial builds one precise version: finite simple-return scenarios, explicit probabilities, long-only fully invested weights, confidence level beta, and strictly positive risk aversion gamma. The implementation and examples are reproducible. They are synthetic teaching evidence, not a historical performance claim.
Before you start
This tutorial is for developers comfortable with weighted averages, vectors, and basic probability. By the end you can calculate a discrete loss tail with partial probability mass and audit a mean-minus-CVaR allocation. All percentages use one common horizon; .05 means 5%, and variance has squared-return units. No live market feed or numerical library is required to run the package examples.
The decision in one sentence
Choose weights that balance probability-weighted return against the average loss in the worst 1-beta probability mass:
Here gamma is not a confidence level. It is the price we choose to put on a unit of tail loss. beta determines which tail is measured. Those controls answer different questions and should never share one unlabeled slider.
Start with loss sign, not a formula
Each input row contains asset returns for one scenario. For portfolio weights w, scenario return and loss are
Positive return therefore becomes negative loss. This is intentional. An all-gain table can have a negative CVaR; clipping it to zero silently changes the definition and the optimizer.
The scenario probability p_t must be supplied and the values must sum to one. The reference code does not normalize an almost-plausible input because normalization can conceal a missing or duplicated scenario. beta=.80 means that CVaR covers the worst .20 probability mass. It does not mean “take the worst 20 rows” unless every row has identical probability.
Why a discrete tail needs partial mass
Take three losses:
| Loss | Probability | Cumulative probability |
|---|---|---|
0.00 | 0.60 | 0.60 |
0.10 | 0.30 | 0.90 |
0.40 | 0.10 | 1.00 |
At beta=.80, the VaR threshold is .10. The tail has mass .20: all .10 probability at loss .40, plus only .10 probability from the .10 row. The result is
It is not .175, the ordinary weighted mean of the two positive-loss rows. It is not .40, the single worst loss. This partial-mass detail is where many attractive but wrong spreadsheet implementations fail.
The threshold-and-slack formulation
Rockafellar and Uryasev’s formulation shows that the finite problem can be written with a threshold z and nonnegative tail slacks u_t:
subject to
Substituting this expression turns the canonical mean-CVaR problem into a linear program. The small reference implementation enumerates LP vertices so that the certificate is inspectable and dependency-free. That is a teaching design, not a recommendation to enumerate vertices for a large institutional universe.
A worked portfolio you can check by hand
Use two assets and four equally likely scenarios:
| Scenario | Asset A | Asset B |
|---|---|---|
| 1 | 24% | 8% |
| 2 | 20% | 6% |
| 3 | 16% | 4% |
| 4 | -20% | -2% |
Set beta=.75 and gamma=.50. If x is the weight in A, the expected return is
Because each row has probability .25, the CVaR at .75 is the worst scenario loss:
The objective is therefore
Its maximum is at x=0: weights [0,1], expected return .04, CVaR .02, and objective .03. At the apparently diversified [.5,.5], expected return is .07, but CVaR is .11, leaving objective .015. This is not proof that B is a universally better asset. It proves only what the declared four-scenario distribution and penalty imply.
Open the guided mean-CVaR lab and move the weight, confidence, and risk-aversion controls separately. Watch the tail membership change when beta crosses a probability boundary.
What the implementation certifies
The Python and TypeScript cores return more than weights:
| Output | Question it answers |
|---|---|
scenarioLosses | Was the return-to-loss sign applied correctly? |
thresholdZ | Where does the declared tail begin? |
tailSlacks | Which losses extend beyond the threshold? |
cvar | What is the direct weighted tail value? |
objective | Does mean - gamma × CVaR reconcile? |
| residuals | Do weights sum to one, respect bounds, and use unit probability? |
iterations | Was the bounded reference search completed? |
The LP candidate is not allowed to certify itself. The implementation recomputes losses, the discrete quantile, CVaR, feasibility residuals, and the final objective. If these disagree beyond tolerance, it returns numerical_issue.
The upstream data gate is part of the algorithm
Before constructing scenarios, I would require an adapter to answer:
- Are security identifiers stable through symbol and exchange changes?
- Are price and quantity on the same split basis?
- Are returns price-only or total return, and how was dividend cash handled?
- Are all local prices translated into the declared base currency with the correct FX direction and timestamp?
- Are scenario dates observation dates, and what information was actually available at each cutoff?
- Were missing rows removed deliberately, and were probabilities rebuilt explicitly afterward?
A mismatched split basis, inverted FX quote, or duplicated dividend can manufacture a tail event. These are hypothetical data failures; the synthetic scenarios below do not establish that any named portfolio experienced them.
Decision table: when this method fits
| Situation | Mean-CVaR response | Better route when it does not fit |
|---|---|---|
| You have explicit stress scenarios and care about severe losses | Good fit: tail mass is visible and weighted | — |
| You only have a covariance matrix | CVaR cannot be reconstructed from covariance alone | Markowitz or GMV with honest moment limits |
| Scenario probabilities are disputed | Run sensitivity cases; do not hide the choice | Robust/distributionally robust optimization |
| Turnover, liquidity, or tax constraints matter | Extend the feasible set and certificate | Execution-aware portfolio construction |
| You want a mean-only portfolio | This package rejects gamma=0 | Use a separately defined mean objective |
| The history mixes split, dividend, or FX bases | Stop before optimization | Repair and revalidate market data |
Failure modes worth testing
beta<=0orbeta>=1: there is no valid tail mass under this contract.gamma<=0: rejected because the package is explicitly the positive-penalty LP variant.- probabilities that do not sum to one: rejected, never silently normalized.
- all gains: negative CVaR is retained rather than clipped.
- reversed signs: treated as different data, not auto-detected.
- too many reference vertices or an exhausted iteration budget:
numerical_issue, not a guessed optimum. - a good certificate on bad scenario history: mathematically valid but economically unusable.
Evidence boundary
The formulas follow the primary Rockafellar–Uryasev CVaR formulation, and the exact examples are independently computed fixtures. A named historical example is deferred until we have licensed point-in-time rows, adjustment status, currencies and FX observations, corporate-action and dividend lineage, scenario probability policy, and a knowledge cutoff. That omission is deliberate: a synthetic example labeled honestly is stronger evidence than a real ticker attached to unreconciled data.
Reproduce, then challenge the answer
From this topic's root folder, run the dependency-free Python example and tests:
python -B examples/run.py
python -B -m unittest discover -s tests -p "test_*.py"
from implementations.python.cvar import mean_cvar
result = mean_cvar(
['A', 'B'],
[[0.24, 0.08], [0.2, 0.06], [0.16, 0.04], [-0.2, -0.02]],
[0.25, 0.25, 0.25, 0.25],
0.75,
0.5,
)
assert result["status"] == "optimal", result
print(result["weights"])
For TypeScript, the runnable entry point imports the canonical core. The commands below use Node.js 22 and this repository’s TypeScript 7 compiler. Compile from the topic folder; if an older compiler rejects --ignoreConfig, omit that flag:
tsc --ignoreConfig --target ES2022 --module NodeNext --moduleResolution NodeNext --strict --skipLibCheck --outDir .topic-build examples/run.ts
node .topic-build/examples/run.js
The shared input fixture is byte-identical to the test fixture. The guided playground runs a bundled copy of the TypeScript core; preset names identify synthetic examples and deliberate failures.
Check your understanding
- Keep
beta=.75and changegammato.20,1/3, and.50. - Compute CVaR for losses
[0,.10,.40], probabilities[.60,.30,.10], andbeta=.80. - Add a 1% return to every asset in every scenario.
Answers. The objective slope in A is .06-.18*gamma: A wins at .20, every mix ties at 1/3, and B wins at .50. The discrete CVaR is .25 using only .10 mass from the threshold row. A common 1% shift raises mean by 1% and lowers CVaR loss by 1%; the objective rises by (1+gamma)*1% at every weight, so the optimizer stays unchanged.
The calculation path
Stop when validation fails. A successful numerical certificate verifies the declared model, not the quality of the market estimates supplied to it.
Numerical scale and the size limit
The reference LP divides every scenario return by its maximum absolute return before solving. Mean-MAD divides the target by the same factor. It then restores return-valued outputs to their original units; weights and probabilities remain unchanged. This prevents fixed absolute tolerances from swallowing tiny return floors or changing the optimum after unit conversion. All-zero rows use scale one.
The enumerator is intentionally bounded at 18 variables. CVaR permits up to 10,000 candidate checks per call; MAD requires the complete candidate set to fit its 200,000-check cap. A count limit or failed finite/objective/feasibility audit returns numerical_issue, with no usable weights. Use a maintained LP solver for larger panels and retain the same independent arithmetic checks. A reported tie is resolved deterministically within the documented floating-point tolerances, not by an economic preference.
What to remember
Mean-CVaR is valuable when the scenario tail is itself a declared, inspectable object. Define loss sign. Treat beta and gamma as separate controls. Count probability mass, including partial mass at a discrete threshold. Audit the optimizer from raw scenario losses. And never let a tail model turn a split, dividend, currency, or timestamp error into a sophisticated-looking portfolio decision.
References
See REFERENCES.md for source roles, access limits, and the historical-evidence decision.
Production handoff checklist
- Freeze the scenario rows, probabilities, return/loss sign, confidence, penalty, horizon, currency, and cutoff.
- Store scenario losses, threshold, slacks, CVaR, mean, objective, and residuals with the weights.
- Re-run the partial-mass tail oracle, all-gain case, bad-probability case, and gamma boundary.
- Compare scenario-probability and confidence sensitivities before interpreting a concentrated solution.
- Route larger panels to an audited LP solver without changing the published mathematical contract.
Continue through Family 01
Rendered from the canonical Mermaid sources linked by this article.
cvar flow
ReferencesPrimary sources and evidence notesExpand the source trail, evidence role, and limitations behind the engineering choices.
Expand the source trail, evidence role, and limitations behind the engineering choices.
All Google and source observations below were made or attempted on 2026-09-14 from an Africa/Cairo browser session. Search results are discovery evidence, not rankings or performance evidence.
Primary and technical sources
| Source | Role | Accessed / supports / limitations |
|---|---|---|
| R. Tyrrell Rockafellar — Risk and Utility in the Duality Framework | Primary author-hosted exposition for loss sign, quantile, superquantile and finite-tail caveats | Accessed as a 19-page PDF with 986 extracted lines; P0–P4 read, especially lines 109–153, 169–182 and 193–230. Supports the variational CVaR definition and discrete-tail caution. Formula/application scope is an author-hosted survey, not a market-data record. |
| Rockafellar and Uryasev — Optimization of Conditional Value-at-Risk DOI metadata | Original-paper attribution | Accessed DOI metadata; publisher full text was not inspected, so no unread equation is claimed. |
| Uryasev publications | Author bibliography | Accessed publication index for attribution; no formula copied from an unread page. |
| Enrico Schumann — Minimising Conditional Value-at-Risk | Finite-scenario LP corroboration | Accessed full 182-line HTML and read variables, objective, scenario constraints and implementation notes. Secondary exposition; direct CVaR recomputation remains the package check. |
| Investor.gov — Ex-Dividend Dates | Upstream dividend timing boundary | Official page inspected for entitlement/payment distinction. It does not validate any supplied scenario history. |
| SEC Investor Bulletin — Stock Splits | Upstream split-unit boundary | Official split mechanics reference; no provider-specific row is asserted. |
Google discovery audit
Search URL:
https://www.google.com/search?udm=14&q=mean-CVaR+portfolio+optimization&hl=en&gl=eg&num=10&pws=0
Google Web results (udm=14) were observed with hl=en, gl=eg; location
shown as Al Hay Al Asher/Nasr City and accessibility link hl=en-EG. Ten
organic positions were recorded. AI Mode, videos, News/Forums modules, and
knowledge-panel text were excluded.
| Rank | Destination observed | Read extent / blocker |
|---|---|---|
| 1 | ScienceDirect mean-CVaR article | Not opened in this pass; discovery only. |
| 2 | MetricGate Mean-CVaR | Not opened; secondary discovery only. |
| 3 | ACM Digital Library result | Not opened; publisher lead, no paywall bypass. |
| 4 | Bjerring CVaR notebook | Not opened; implementation lead only. |
| 5 | Springer mean-CVaR result | Not opened; date-sensitive discovery only. |
| 6 | MPRA working paper | Not opened; working-paper lead only. |
| 7 | RePEc record | Not opened; bibliography only. |
| 8 | arXiv result | Not opened in this pass; primary-research lead, not used as proof. |
| 9 | ResearchGate applied study | Not opened; secondary host, no device/paywall bypass. |
| 10 | Quantstock explainer | Not opened; secondary discovery only. |
The ten positions satisfy the requested Google observation gate, but unopened destinations are not treated as read evidence. No CAPTCHA, paywall, device check, or access barrier was bypassed.
Contextual prior method
The prior D02-F01-A01 Backward Split Adjustment is relevant because a fabricated split loss can enter a tail table. It is a data-basis precedent, not a claim that the synthetic A04 rows came from that module. The root-owned ledger records the full source-role and prior-case boundaries.
Source roles
- Accessed: Google result page and the listed readable sources on 2026-09-14, Africa/Cairo; each result row records the exact extent or unavailable state.
- Supports: Rockafellar’s primary-author exposition supports the loss,
quantile and CVaR mathematics. Schumann corroborates a finite LP structure.
The
.1/.25tail, canonical optimizer and all-gain case are independent synthetic calculations and implementation choices. - Limitations: no historical market series, provider snapshot, or performance result is cleared; unread/blocked search destinations provide no technical claim; numeric vectors cannot automatically detect mislabeled returns/losses or split/FX/dividend semantics.
Independent recheck — 2026-09-16
Rockafellar’s author-hosted 2018 survey, pp. 2–3, equation 2.6: variational superquantile and discrete-tail caveat.
Recheck scope: theory and attribution only. Fixtures and solver limits are author-derived calculations and implementation choices. No new historical-market or personal-experience evidence is asserted.
Full dependency-light reference implementations in both supported languages.
export interface MeanCvarResult {
branch?: string;
status: string;
variant: string;
method: string;
assetIds: string[] | null;
weights: number[] | null;
expectedReturn: number | null;
cvar: number | null;
thresholdZ: number | null;
tailSlacks: number[] | null;
scenarioLosses: number[] | null;
objective: number | null;
budgetResidual: number | null;
lowerBoundResidual: number | null;
probabilityResidual: number | null;
iterations: number;
maxIterations: number;
solutionClass: string;
diagnostics: Record<string, unknown>;
warnings: string[];
}
const DEFAULT_MAX_ITERATIONS = 10000;
const TOL_PROBABILITY = 1e-12;
const TOL_FEASIBILITY = 1e-10;
const TOL_OBJECTIVE = 1e-10;
const MAX_VERTEX_VARIABLES = 18;
const MAX_VERTEX_CANDIDATES = 200000;
function finiteReal(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value);
}
function dot(left: number[], right: number[]): number {
return left.reduce((sum, value, index) => sum + value * right[index], 0);
}
function solveLinear(matrix: number[][], vector: number[]): number[] | null {
const size = vector.length;
const augmented = matrix.map((row, index) => [...row, vector[index]]);
for (let column = 0; column < size; column += 1) {
let pivot = column;
for (let row = column + 1; row < size; row += 1)
if (Math.abs(augmented[row][column]) > Math.abs(augmented[pivot][column]))
pivot = row;
if (Math.abs(augmented[pivot][column]) <= 1e-13)
return null;
[augmented[column], augmented[pivot]] = [augmented[pivot], augmented[column]];
const divisor = augmented[column][column];
for (let j = column; j <= size; j += 1)
augmented[column][j] /= divisor;
for (let row = 0; row < size; row += 1) {
if (row === column)
continue;
const factor = augmented[row][column];
if (factor === 0)
continue;
for (let j = column; j <= size; j += 1)
augmented[row][j] -= factor * augmented[column][j];
}
}
return augmented.map((row) => row[size]);
}
function combinations(size: number, choose: number): number[][] {
const output: number[][] = [];
const current: number[] = [];
const visit = (start: number): void => {
if (current.length === choose) {
output.push(current.slice());
return;
}
for (let index = start; index <= size - (choose - current.length); index += 1) {
current.push(index);
visit(index + 1);
current.pop();
}
};
visit(0);
return output;
}
export function weightedVarCvar(losses: number[], probabilities: number[], beta: number): {
threshold: number;
cvar: number;
} {
const ordered = losses.map((loss, index) => ({ loss, probability: probabilities[index] })).sort((a, b) => a.loss - b.loss);
let cumulative = 0;
let threshold = ordered[ordered.length - 1].loss;
for (const item of ordered) {
cumulative += item.probability;
if (cumulative >= beta) {
threshold = item.loss;
break;
}
}
const cvar = threshold + losses.reduce((sum, loss, index) => sum + probabilities[index] * Math.max(0, loss - threshold), 0) / (1 - beta);
return { threshold, cvar };
}
function failure(status: string, message: string, details: Partial<MeanCvarResult> = {}, diagnostics: Record<string, unknown> = {}): MeanCvarResult {
return { status, variant: "mean-cvar-positive-gamma-long-only", method: "lp-vertex-enumeration", assetIds: details.assetIds ?? null, weights: null, expectedReturn: null, cvar: null, thresholdZ: null, tailSlacks: null, scenarioLosses: null, objective: null, budgetResidual: null, lowerBoundResidual: null, probabilityResidual: details.probabilityResidual ?? null, iterations: details.iterations ?? 0, maxIterations: details.maxIterations ?? DEFAULT_MAX_ITERATIONS, solutionClass: "unknown", diagnostics: { message, ...diagnostics }, warnings: [] };
}
function meanCvarNormalized(assetIdsInput: unknown, returnsInput: unknown, probabilitiesInput: unknown, betaInput: unknown, gammaInput: unknown, options: {
maxIterations?: unknown;
} = {}): MeanCvarResult {
if (!Array.isArray(assetIdsInput))
return failure("invalid_input", "assetIds must be an ordered array.");
const assetIds = assetIdsInput.slice();
if (!assetIds.length || assetIds.some((value) => typeof value !== "string" || value.trim() === ""))
return failure("invalid_input", "assetIds must contain non-empty strings.");
if (new Set(assetIds).size !== assetIds.length)
return failure("invalid_input", "assetIds must be unique.");
const size = assetIds.length;
if (!Array.isArray(returnsInput) || returnsInput.length === 0)
return failure("invalid_input", "returns must be a non-empty T by N matrix.", { assetIds });
if (returnsInput.some((row) => !Array.isArray(row) || row.length !== size))
return failure("invalid_input", "each return row must align to assetIds.", { assetIds });
if ((returnsInput as unknown[][]).some((row) => row.some((value) => !finiteReal(value))))
return failure("invalid_input", "returns must contain finite real numbers.", { assetIds });
const returns = (returnsInput as unknown[][]).map((row) => row.slice() as number[]);
const observations = returns.length;
if (!Array.isArray(probabilitiesInput) || probabilitiesInput.length !== observations)
return failure("invalid_input", "probabilities must align to return rows.", { assetIds });
if ((probabilitiesInput as unknown[]).some((value) => !finiteReal(value) || value < 0))
return failure("invalid_input", "probabilities must be finite and non-negative.", { assetIds });
const probabilities = (probabilitiesInput as unknown[]).slice() as number[];
const probabilityResidual = Math.abs(probabilities.reduce((sum, value) => sum + value, 0) - 1);
if (probabilityResidual > TOL_PROBABILITY)
return failure("invalid_input", "probabilities must sum to one; no silent normalization is applied.", { assetIds, probabilityResidual });
if (!finiteReal(betaInput) || betaInput <= 0 || betaInput >= 1)
return failure("invalid_input", "beta must be a confidence level strictly between zero and one.", { assetIds });
const beta = betaInput;
if (!finiteReal(gammaInput) || gammaInput <= 0)
return failure("invalid_input", "gamma must be strictly positive in this bounded LP variant.", { assetIds });
const gamma = gammaInput;
const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
if (!Number.isInteger(maxIterations) || (maxIterations as number) < 0 || (maxIterations as number) > DEFAULT_MAX_ITERATIONS)
return failure("invalid_input", "maxIterations must be an integer from 0 through 10,000.", { assetIds });
const iterationBudget = maxIterations as number;
if (iterationBudget === 0)
return failure("numerical_issue", "maxIterations=0 cannot produce an LP certificate.", { assetIds, maxIterations: iterationBudget });
const variables = size + 1 + observations;
if (variables > MAX_VERTEX_VARIABLES)
return failure("numerical_issue", "the deterministic vertex core is bounded at 18 variables.", { assetIds, maxIterations: iterationBudget }, { variables, maxVertexVariables: MAX_VERTEX_VARIABLES });
const meanReturns = Array.from({ length: size }, (_value, index) => returns.reduce((sum, row, scenario) => sum + probabilities[scenario] * row[index], 0));
const inequalities: Array<{
a: number[];
b: number;
}> = [];
for (let index = 0; index < size; index += 1) {
const lower = Array(variables).fill(0);
lower[index] = -1;
inequalities.push({ a: lower, b: 0 });
const upper = Array(variables).fill(0);
upper[index] = 1;
inequalities.push({ a: upper, b: 1 });
}
for (let scenario = 0; scenario < observations; scenario += 1) {
const lower = Array(variables).fill(0);
lower[size + 1 + scenario] = -1;
inequalities.push({ a: lower, b: 0 });
const tail = Array(variables).fill(0);
for (let index = 0; index < size; index += 1)
tail[index] = -returns[scenario][index];
tail[size] = -1;
tail[size + 1 + scenario] = -1;
inequalities.push({ a: tail, b: 0 });
}
const equality = Array(variables).fill(0);
for (let index = 0; index < size; index += 1)
equality[index] = 1;
const objective = Array(variables).fill(0);
meanReturns.forEach((value, index) => { objective[index] = value / Math.max(1, gamma); });
objective[size] = -gamma / Math.max(1, gamma);
probabilities.forEach((value, scenario) => { objective[size + 1 + scenario] = -(gamma / Math.max(1, gamma)) * value / (1 - beta); });
const activeCount = variables - 1;
const combinationCount = activeCount <= inequalities.length ? chooseCount(inequalities.length, activeCount) : 0;
if (combinationCount > MAX_VERTEX_CANDIDATES)
return failure("numerical_issue", "the LP vertex count exceeds the deterministic candidate limit.", { assetIds, maxIterations: iterationBudget }, { verticesRequired: combinationCount, maxVertexCandidates: MAX_VERTEX_CANDIDATES });
const budget = Math.min(iterationBudget, combinationCount);
const activeSets = combinations(inequalities.length, activeCount);
let bestValue = -Infinity;
let bestSolution: number[] | null = null;
let checked = 0;
for (const active of activeSets) {
if (checked >= budget)
break;
checked += 1;
const candidate = solveLinear([equality, ...active.map((index) => inequalities[index].a)], [1, ...active.map((index) => inequalities[index].b)]);
if (candidate === null || candidate.some(x => !Number.isFinite(x)) || inequalities.some((constraint) => dot(constraint.a, candidate) > constraint.b + TOL_FEASIBILITY))
continue;
const value = dot(objective, candidate);
if (bestSolution === null || value > bestValue + TOL_OBJECTIVE * Math.max(Math.abs(value), Math.abs(bestValue), 1e-300)) {
bestValue = value;
bestSolution = candidate;
}
}
if (checked < combinationCount)
return failure("numerical_issue", "maxIterations exhausted before all LP vertices were checked.", { assetIds, maxIterations: iterationBudget, iterations: checked }, { verticesChecked: checked, verticesRequired: combinationCount });
if (bestSolution === null)
return failure("numerical_issue", "no feasible LP vertex was found.", { assetIds, maxIterations: iterationBudget, iterations: checked });
const weights = bestSolution.slice(0, size);
const thresholdFromLp = bestSolution[size];
const scenarioLosses = returns.map(row => -dot(row, weights));
const expectedReturn = dot(meanReturns, weights);
const { threshold, cvar } = weightedVarCvar(scenarioLosses, probabilities, beta);
const tailSlacks = scenarioLosses.map(loss => Math.max(0, loss - threshold));
const objectiveValue = expectedReturn - gamma * cvar;
const lpObjective = bestValue * Math.max(1, gamma);
const maxTailResidual = Math.max(0, ...scenarioLosses.map((loss, t) => loss - thresholdFromLp - bestSolution![size + 1 + t]));
const lowerResidual = Math.max(0, -Math.min(...weights));
const budgetResidual = Math.abs(weights.reduce((a, b) => a + b, 0) - 1);
const primalResidual = Math.max(maxTailResidual, lowerResidual, budgetResidual);
const objectiveScale = Math.max(Math.abs(lpObjective), Math.abs(objectiveValue), Math.abs(expectedReturn), Math.abs(gamma * cvar), 1e-15);
const objectiveGap = Math.abs(lpObjective - objectiveValue);
if (![lpObjective, objectiveValue, cvar, objectiveGap].every(Number.isFinite) || primalResidual > TOL_FEASIBILITY || objectiveGap > TOL_OBJECTIVE * objectiveScale)
return failure('numerical_issue', 'the selected LP vertex failed the independent primal/objective audit.', { assetIds, maxIterations: iterationBudget, iterations: checked }, { verticesChecked: checked, verticesRequired: combinationCount, primalResidual, objectiveGap, objectiveScale, lpObjective, directObjective: objectiveValue });
const tiedWeights: number[][] = [];
for (const active of activeSets) {
const candidate = solveLinear([equality, ...active.map(i => inequalities[i].a)], [1, ...active.map(i => inequalities[i].b)]);
if (candidate === null || candidate.some(x => !Number.isFinite(x)) || inequalities.some(c => dot(c.a, candidate) > c.b + TOL_FEASIBILITY))
continue;
const ws = candidate.slice(0, size);
const losses = returns.map(row => -dot(row, ws));
const value = dot(meanReturns, ws) - gamma * weightedVarCvar(losses, probabilities, beta).cvar;
if (Math.abs(value - objectiveValue) <= TOL_OBJECTIVE * Math.max(Math.abs(value), Math.abs(objectiveValue), 1e-300) && !tiedWeights.some(prior => Math.max(...ws.map((w, j) => Math.abs(w - prior[j]))) <= 1e-9))
tiedWeights.push(ws);
}
return { status: 'optimal', variant: 'mean-cvar-long-only', method: 'lp-vertex-enumeration', branch: 'positive_gamma_lp', assetIds, weights, expectedReturn, cvar, thresholdZ: threshold, tailSlacks, scenarioLosses, objective: objectiveValue, budgetResidual, lowerBoundResidual: lowerResidual, probabilityResidual, iterations: checked, maxIterations: iterationBudget, solutionClass: tiedWeights.length > 1 ? 'non_unique' : 'unique', diagnostics: { beta, gamma, meanReturns, tailMass: 1 - beta, lpObjective, objectiveGap, objectiveScale, directObjective: objectiveValue, primalResidual, certificate: 'lp-vertex-enumeration', verticesChecked: checked, verticesRequired: combinationCount, thresholdFromLp, quantileFromLosses: threshold, distinctOptimalWeights: tiedWeights.length }, warnings: [] };
}
export const meanCvarOptimization = meanCvar;
export function meanCvar(assetIdsInput: unknown, returnsInput: unknown, probabilitiesInput: unknown, betaInput: unknown, gammaInput: unknown, options: {
maxIterations?: unknown;
} = {}): MeanCvarResult {
let scale = 1;
let rows = returnsInput;
if (Array.isArray(rows) && rows.length && rows.every(row => Array.isArray(row) && row.length && row.every(x => typeof x === 'number' && Number.isFinite(x)))) {
scale = rows.reduce((m: number, row: number[]) => row.reduce((n, x) => Math.max(n, Math.abs(x)), m), 0) || 1;
rows = rows.map((row: number[]) => row.map(x => x / scale));
}
const result = meanCvarNormalized(assetIdsInput, rows, probabilitiesInput, betaInput, gammaInput, options);
const restore = (value: unknown): unknown => Array.isArray(value) ? value.map(restore) : typeof value === 'number' ? value * scale : value;
const record = result as unknown as Record<string, unknown>;
for (const key of ["expectedReturn", "cvar", "thresholdZ", "tailSlacks", "scenarioLosses", "objective"])
if (record[key] !== null && record[key] !== undefined)
record[key] = restore(record[key]);
for (const key of ["meanReturns", "lpObjective", "objectiveGap", "objectiveScale", "directObjective", "thresholdFromLp", "quantileFromLosses"])
if (key in result.diagnostics)
result.diagnostics[key] = restore(result.diagnostics[key]);
result.diagnostics.returnScale = scale;
const allFinite = (value: unknown): boolean => typeof value === 'number' ? Number.isFinite(value) : Array.isArray(value) ? value.every(allFinite) : value !== null && typeof value === 'object' ? Object.values(value).every(allFinite) : true;
if (result.status === 'optimal' && !allFinite(result))
return failure('numerical_issue', 'output arithmetic exceeded finite return units.', { assetIds: assetIdsInput as string[] });
return result;
}
function chooseCount(n: number, k: number): number { let v = 1; for (let i = 1; i <= k; i++)
v = v * (n - k + i) / i; return Math.round(v); }
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.
