RSJ Portfolio Portfolio Optimization

← All posts

2026-10-06

How to Read Optimizer Allocation Weights and Risk Model Output

How to Read Optimizer Allocation Weights and Risk Model Output
portfolio optimization modern portfolio theory portfolio correlation portfolio optimization hierachical risk parity minimize volatility minimize semivariance maximize sharpe

How to Read Optimizer Allocation Weights and Risk Model Output

You ran the optimizer in RSJ Portfolio. It converged, and now there is a table in front of you: one allocation weight per candidate, a risk figure for the portfolio, and a handful of model diagnostics. Nothing in that table says “this is a good portfolio” — you have to read it.

This guide is about exactly that: turning a finished optimization run into an interpretation you can defend. By the end you will be able to read the weights column against the objective you selected, decode the risk model output (volatility, semi-variance, CVaR and the per-asset risk contributions), and run a short verification checklist before you act on the result.

This article assumes you already have a result. If you still need to get prices in, start with How to Import End-of-Day Stock Prices Into a Portfolio Optimizer. If you are still deciding which objective to run, see Minimum Semivariance vs Minimum Volatility: Which Objective?.

Prerequisites: what to have in hand before you interpret anything

  • A completed optimization run in RSJ Portfolio, with a candidate list and one selected objective (minimum volatility, maximum Sharpe ratio, minimum semi-variance, hierarchical risk parity, or minimum CVaR).
  • Knowledge of which risk model and return model you used. The output metrics are produced by that model. Reading a semi-variance figure as if it were a standard deviation is the most common misreading.
  • A mental model of weights. Long-only weights are non-negative and sum to 1 (100%). Anything else means a constraint or an objective you did not intend.
  • Access to the recommendation view, where the optimizer shows the resulting allocation together with the estimated risk figures.

Before you start reading output, write the run parameters down. Future you will not remember them:

# run-notes.yaml - fill this in from the optimizer configuration
run: 2026-10-06-watchlist-a
objective: minimum-volatility
risk_model: sample-covariance
return_model: none
constraints:
  long_only: true
  max_weight_per_asset: 0.10
data:
  provider: <your configured provider>
  start: 2019-01-01
  end: 2026-09-30

Step 1: Locate the allocation weights and the risk metrics

After the run finishes, open the recommendation view. You get two things side by side, which is the whole point:

  • The allocation table — one row per candidate, with its weight, normally ordered by size so the largest positions are on top.
  • The risk output block — the portfolio-level risk figure for the objective you chose (volatility, semi-variance, CVaR), plus the per-asset risk contributions and, depending on the run, the correlation information the allocation was built from.

Two things worth internalising here:

  1. Weights are the decision; risk figures are the audit trail. A weight tells you what the optimizer did. A risk figure tells you how much it expects that decision to cost you.
  2. The output is model-dependent, not absolute. Run the same candidate list with a different risk estimator and the weights will move. That is expected, not a bug.

The hard way to get the same table is to pull prices yourself and hand-roll a constrained quadratic program in NumPy, then write your own risk-contribution reporting on top of it. RSJ Portfolio produces both the allocation and the risk output from one run.

Step 2: Interpret the weights in the context of your objective

A weight is not a forecast. It is the fraction of capital the optimizer assigned to make the objective function better. That single sentence resolves most confusion.

  • Minimum volatility: expect the capital spread toward candidates whose returns correlate weakly with each other — or whose downside correlates weakly. Low-correlation assets are what reduce the portfolio’s estimated volatility, so they get weight even if their individual volatility is unremarkable.
  • Maximum Sharpe ratio: weights now depend on a return model as well. A large weight can mean “good risk-adjusted return estimate”, not “low risk”.
  • Hierarchical risk parity: weights follow the cluster structure of the correlation matrix rather than a single global optimisation, so the distribution tends to look more evenly spread.
  • Concentration check: if three positions hold 70% of the capital, ask whether that reflects genuine diversification limits or a binding constraint. The optimizer rarely concentrates without a reason.

Then verify the constraints actually held. This is a five-line check on the exported result:

import csv

with open("rsj-portfolio-weights.csv", newline="") as f:
    rows = list(csv.DictReader(f))  # adjust column names to your export

total = sum(float(r["weight"]) for r in rows)
MAX_WEIGHT = 0.10  # the cap you configured

print(f"positions: {len(rows)}")
print(f"sum of weights: {total:.6f}")

breaches = [r["symbol"] for r in rows if float(r["weight"]) > MAX_WEIGHT + 1e-6]
print("constraint breaches:", breaches or "none")

If the sum is 1.0 and there are no breaches, the weights are internally consistent. Now you can interpret them.

Step 3: Read the risk model output: volatility, semi-variance, CVaR

The risk number at the bottom of the recommendation view is the estimated risk of the whole portfolio under the model you selected — not the sum of the individual assets’ risks. That gap between the two is the effect you are paying for.

  • Volatility (standard deviation). Total risk, upside and downside treated alike. Lower is less volatile. If you optimised for minimum volatility, this is the number to compare against alternative runs.
  • Semi-variance. Only deviations below a target count. If you chose minimum semi-variance, this is your objective value; a volatility figure here would be a side metric.
  • CVaR (conditional value at risk). Estimates the average loss in the tail beyond a chosen confidence level. It answers “how bad is a bad day”, not “how bumpy is the ride”. Use it if you optimised for minimum CVaR.
  • Per-asset risk contribution. The share of total portfolio risk attributable to each position. This is the part most people skip and it is often the most informative.

Weights and risk contributions are not the same quantity. A 10% position in a high-volatility, weakly-correlated asset can contribute more than 30% of the portfolio’s risk. Reconstruct the split from the export:

import numpy as np
import pandas as pd

w = pd.read_csv("weights.csv", index_col="symbol")["weight"]
cov = pd.read_csv("covariance.csv", index_col=0)  # model output, same ordering

wv = w.to_numpy()
port_var = wv @ cov.to_numpy() @ wv
port_vol = np.sqrt(port_var)

marginal = cov.to_numpy() @ wv / port_vol
contrib = wv * marginal / port_vol          # sums to 1
print(pd.Series(contrib, index=w.index).sort_values(ascending=False).round(4))

If one row dominates that output while its weight is modest, you found your portfolio’s actual risk driver.

Step 4: Validate the result against correlation and asset-level risk

The product’s core claim is statistical: by allocating to candidates that correlate weakly — or whose downside correlates weakly — the resulting portfolio is less volatile than its parts. The output should demonstrate that. Check it three ways.

  1. Diversification benefit. Compare the portfolio risk figure to the weighted average of the individual assets’ risk figures. The portfolio should be lower. If it is not, you have assembled a basket that behaves like one asset.
  2. Correlation structure. Review the correlation matrix for the selected assets. You want low or negative pairwise values among the positions that carry weight.
  3. Risk concentration. Cross-check Step 3: no single asset should dominate total risk unless you intended it.
import pandas as pd

corr = pd.read_csv("correlation.csv", index_col=0)
w = pd.read_csv("weights.csv", index_col="symbol")["weight"]
held = w[w > 0.001].index

sub = corr.loc[held, held].copy()
sub.values[[range(len(held))] * 2] = float("nan")  # drop the diagonal
print("max pairwise correlation among held assets:", round(sub.max().max(), 3))

If that maximum is high, or portfolio risk is not below the weighted average of asset risks, do not start tweaking weights by hand. Revisit the two upstream decisions: the risk model, and the data. Stale or missing prices distort every correlation estimate. The cleaning pipeline and cache in RSJ Portfolio exist to keep that from happening silently — if the date range or provider looks wrong, fix that first.

Step 5: Confirm it worked: the verification checklist

Run through this before you act on the allocation:

  • [ ] Weights sum to 100% (1.0), within rounding.
  • [ ] All constraints hold — max weight per asset, long-only, any group limits you configured.
  • [ ] No unexpected sign — no negative weights in a long-only run.
  • [ ] Portfolio risk < weighted average of asset risk — the diversification benefit is present.
  • [ ] Risk contributions are proportional enough — no unintended single-asset risk dominance.
  • [ ] The result matches the objective — a minimum-volatility run has the lowest estimated volatility among the variants you tested.
  • [ ] Every held asset has a plausible reason to be there — low correlation, not just a familiar name.
# verify.py - run after each export
import pandas as pd

w = pd.read_csv("weights.csv", index_col="symbol")["weight"]
risk = pd.read_csv("risk_summary.csv", index_col=0)["value"]

checks = {
    "weights sum to 1": abs(w.sum() - 1.0) < 1e-6,
    "long only": (w >= -1e-9).all(),
    "max weight respected": w.max() <= 0.10 + 1e-6,
    "risk below asset average": risk["portfolio"] < risk["asset_weighted_average"],
}
for name, ok in checks.items():
    print(f"[{'PASS' if ok else 'FAIL'}] {name}")

Any FAIL is a signal to re-run with corrected configuration, not to hand-edit the output.

Troubleshooting common issues when reading output

Weights are all zero, or the sum is wrong. The feasible region is empty or the candidate list is empty. Check that your candidate list actually contains symbols and that your constraints can be satisfied simultaneously — a 5% cap with 10 candidates is infeasible.

Risk metrics look unrealistic. Almost always data. Verify the date range, the provider, and whether the cleaning pipeline dropped observations. Missing or stale end-of-day prices inflate or deflate every estimate downstream.

Unexpected concentration in a few assets. Open the correlation matrix. If most candidates are highly correlated, the optimizer has few genuine diversifiers left to choose from and concentrates in the ones that remain. Widen the candidate list or loosen constraints.

The output doesn’t match what you expected. Confirm which objective and which risk model produced it. RSJ Portfolio lets you switch both and re-run, and the two runs are only comparable if they differ in one dimension at a time.

A metric you expected isn’t shown. The output block displays the risk measure relevant to your objective. Switch the objective to see the corresponding measure.

FAQ

What does a negative allocation weight mean?

If your optimization allows short selling, a negative weight represents a short position. If shorting is disabled — the default — all weights should be non-negative. RSJ Portfolio respects the weight constraints you set, so check your configuration if you see unexpected negatives.

How do I interpret the risk model output if I used a non-standard risk model?

The output metrics (volatility, semi-variance, CVaR) correspond to the risk model you selected. RSJ Portfolio displays the relevant risk measure for your objective. If you are unsure, re-run with a standard model to compare.

Can I export the allocation weights and risk metrics for further analysis?

Yes. RSJ Portfolio supports exporting the results, including weights and risk figures, so you can analyse them in your own tools or share them with stakeholders.

Why do the weights sometimes look concentrated in a few assets?

Concentration can occur if the optimizer finds that a few assets provide the best risk-adjusted return, or if constraints limit diversification. Review the correlation matrix and consider adjusting constraints or expanding the candidate list.

Conclusion: turning output into decisions

Reading optimizer output is not about finding the “right” number — there isn’t one. It is about understanding which trade-off the optimizer made on your behalf: how much capital went where, which assets carry the risk, and whether the diversification you were promised actually shows up in the figures.

Use the checklist. If weights sum correctly, constraints hold, portfolio risk sits below the weighted average of asset risk, and no single position dominates risk unintentionally, the result is sound enough to act on. If any of those fail, the problem is upstream — the model, the constraints, or the data — not the weights.

RSJ Portfolio is built to make this loop short: weights and risk figures presented together, with selectable objectives, risk models and weight constraints, so you can change one variable, re-run, and read the difference. Start with the overview at portfolio.rsj.de and the user guide, then run a second configuration — minimum semi-variance against minimum volatility is the most instructive first comparison. Access is sold as a single-user license; the pricing page describes what it covers.

Related posts

EU label: AI-generated content