Skip to content

mpt.* ​

How do you size a portfolio by risk parity, or by the highest Sharpe ratio, inside a script? Collect each name's daily returns into one flat array, then hand a window of it to mpt.weights with the method's name. It returns weights that sum to 1, which you can pass straight to book.target.

ameno
indicator("Risk parity, monthly")
array<string> names = array.from("SPY", "TLT", "GLD", "BTC-USD")
int window = input.int(90, "Window, days")
// gaps_on: a day an asset doesn't trade reads na, not the day before
array<float> r = request.each(names, close / close[1] - 1,
  gaps = barmerge.gaps_on)
// Every day's returns, name after name
var array<float> R = array.new_float(0)
// The weights call keeps its state here: leave it alone
var array<float> basis = array.new_float(0)
var int days = 0
for x in r
    array.push(R, x)
days += 1
if days > window and timeframe.change("1M")
    array<bool> on = array.new_bool(array.size(names), true)
    array<float> w = mpt.weights("risk_parity", R, array.size(names),
      days - window, days, on, basis)
    book.target(names, w, reason = "risk parity")
plot(book.equity(), "Equity")
plot(mpt.capped(basis), "Solves stopped short")

Run it on a daily chart of a coin that trades every day, such as CRYPTO:BTCUSD. Bitcoin's weekend returns arrive only on weekend bars, so on a stock's chart, which has none, they would never reach R.

mpt.*, book.* and request.each are AmenoScript only, and any one of them makes this script AmenoScript. mpt.* reads only its arguments, so it can't see a later bar than the returns you gave it.

The window ​

Every mpt.* call reads the same shape of input:

ArgumentMeaning
returnsA flat array<float>: n numbers a day, the names in the same order every day.
nHow many names, up to 1,000.
lo, hiThe days to read: lo to hi - 1, counted from 0.
onAn array<bool>, one per name: which names count.

A name that is off, or has no reading in the window, gets weight 0 from mpt.weights and na from mpt.read. A na return counts as no reading in a mean or a covariance, and as 0 in anything that walks the days, such as a drawdown.

Measures ​

mpt.read(measure, returns, n, lo, hi, on) gives one number per name, and mpt.score(measure, returns, n, lo, hi, weights) gives one number for a portfolio. The word that reads a measure is the word that optimises it, so changing a portfolio's objective is changing one word.

MeasureReadsOptions
mean, volThe mean and the standard deviation
downsideThe root mean square of the losses below the thresholdthreshold
sharpe, sortinoThe excess mean over volatility, or over downsidethreshold
omega, gain_to_painGains over losses around the threshold, or their sum over the lossesthreshold
kappaThe excess mean over the root of the lower partial moment of orderthreshold, order
var, cvarThe loss at the alpha quantile, and the mean loss past italpha
starr, rachevThe excess mean over CVaR, and the gain tail's mean over the loss tail'sthreshold, alpha
max_drawdown, cdar, ulcerThe deepest drawdown, the mean of the worst 1 - alpha of them, and their root mean squarecompound; alpha for cdar
calmar, martinThe excess mean over the deepest drawdown, or over the Ulcer indexthreshold, compound
beta, alpha, treynorAgainst a benchmark, at the thresholdbenchmark, threshold
tracking_error, informationThe active return's volatility, and its mean over thatbenchmark

A measure is per bar, not annualised. Volatility and covariance divide by the number of days read, not one fewer, where cs.zscore uses the sample standard deviation. threshold is 0 by default, alpha 0.95 and order 2. A drawdown reads the curve of summed returns unless compound = true asks for the compounded curve a chart draws. A ratio with nothing under it reads na.

More reads:

FunctionReturns
mpt.mean(...), mpt.vol(...)Each name's mean and volatility. mpt.mean takes estimator = "bayes_stein".
mpt.cov(...), mpt.corr(...)A matrix, with estimator = "sample" or "ledoit_wolf".
mpt.psd(cov, on)A covariance mpt.weights can take as cov. One you build pair by pair, each pair over its own days, may not factor, and mpt.weights stops the run on it. mpt.psd returns a covariance that factors unchanged. Otherwise it sets its correlation's negative eigenvalues to 0 and keeps each name's own variance.
mpt.bets(returns, n, lo, hi, on)How many names' worth of bet each name carries: the sum of its positive correlations with the names that are on, itself included. A name that moves with no other reads 1.
mpt.returns_of(returns, n, lo, hi, weights)The portfolio's own daily returns.
mpt.contributions("risk" or "return", returns, n, lo, hi, weights)The portfolio's volatility or mean, split by name. The parts sum to the whole.
mpt.running_mean(target, state)target folded into a running mean whose step is its own mean turnover so far, so no length is chosen. state is a var array<float>.

Weights ​

mpt.weights(method, returns, n, lo, hi, on, basis, ...) gives weights that sum to 1, long only unless lower says otherwise. basis is a var array<float>, empty at the start, that the call keeps its state in. Give each call its own, and never write to it.

MethodFinds
equal, inverse_volEqual weights, or weights in proportion to 1 / volatility.
risk_parityEqual risk contributions, or the shares in budgets.
hrpHierarchical risk parity.
min_variance (or vol)The least variance.
utilityThe largest mean less half risk_aversion times the variance.
sharpe, information, diversificationThe largest ratio.
mean, alphaThe largest score.
beta, treynorThe lowest absolute beta, or the largest Treynor ratio.
omega, gain_to_painThe largest ratio. Both give the same weights, since Omega is 1 plus gain to pain.
cvar, cdar, max_drawdown, downside, ulcerThe least of it.
sortino, kappa, starr, calmar, martin, rachevThe largest ratio.

Every method is deterministic, with no random draw. Most are solved exactly. ulcer and martin are solved to a small gap, rachev by a local search because it isn't convex, and the drawdown words with compound = true start from the exact answer on the summed curve and improve it locally.

Options ​

Pass these by name. An option a method can't honour stops the run and says so.

OptionWhat it does
lower, upperBounds on each weight: one number, or one per name. A negative lower allows shorts.
grossA cap on the sum of absolute weights.
groups, group_lower, group_upperOne group number per name, and bounds on each group's total.
heldThe weights you hold now, an array<float>. max_turnover and turnover measure change from it, or from the last weights in basis when it's left out.
max_turnoverA cap on the sum of absolute changes.
turnoverA charge per unit of change, on min_variance, utility, cvar, cdar, max_drawdown and ulcer. A ratio can't take a charge; use max_turnover.
target_returnOn a risk word: the least risk with at least this mean.
target_riskOn mean: the largest mean with at most this risk.
threshold, alpha, benchmark, order, risk_aversion, estimatorThe measure's own inputs.
mean, covYour own estimates, from any estimator, mpt.black_litterman included.
budgetsRisk shares for risk_parity.

mpt.frontier(returns, n, lo, hi, on, points, ...) gives one row per point of the efficient frontier, from the least risk to the highest mean: the weights, then the volatility, then the mean. mpt.black_litterman(cov, prior, views, targets, tau, risk_aversion, view_variance) gives a Black-Litterman mean to pass as mean. It starts from the mean that the market weights prior imply under cov and risk_aversion, with tau for how unsure that is. Each row of views weights the names for one view, targets gives the return each view expects, and view_variance, if given, how sure each view is. The reference has each argument.

When there's no ordinary answer ​

mpt.status(basis) says what the last call found. mpt.capped(basis) counts every time a solve on that basis stopped short of the optimum at its limit on steps or precision, over all its calls. A healthy run reads 0 there, so plot it while you build a script.

mpt.statusHappens whenThe weights
solvedThe optimum.
limits can't be metThe bounds, groups or a target_return rule out every mix.All 0.
no mix has a positive excess meanA ratio's top is below zero for every allowed mix.The mix with the least of the ratio's own risk, such as the least volatility for sharpe; omega with no limit gives its best single name.
no risk to reduceThe returns have no variance.Equal for min_variance, risk_parity and hrp; where they started for the rest.
target out of reachtarget_risk is below the least risk there is.The least-risk portfolio.
singular: ties went to the most equal weightsThe covariance is singular.The most equal of the tied answers.
stopped shortAn iterative method hit its limit.Its best so far.

When no name counts, every weight is 0, and one name that counts takes everything.

Two standalone helpers ​

mpt.omega_weights(returns, n, lo, hi, on, basis) gives Omega's exact optimum with no limits, keeping its last answer in basis so the next day starts from it. mpt.inverse_vol(returns, n, lo, hi, on) gives inverse-volatility shares, and reads a na as no reading rather than a 0. Both compare as the language does, so returns within 1e-10 of each other count as equal.

Traps ​

  • Each call needs its own basis. A basis another call wrote, or that your script changed, stops the run.
  • The window must hold n numbers a day. Push one value per name every day, na included, or every later day reads the wrong names.

See also ​

AmenoScript is compatible with Pine Script v6. Pine Script is a trademark of TradingView, Inc., which has no part in AmenoScript.