Appearance
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:
| Argument | Meaning |
|---|---|
returns | A flat array<float>: n numbers a day, the names in the same order every day. |
n | How many names, up to 1,000. |
lo, hi | The days to read: lo to hi - 1, counted from 0. |
on | An 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.
| Measure | Reads | Options |
|---|---|---|
mean, vol | The mean and the standard deviation | |
downside | The root mean square of the losses below the threshold | threshold |
sharpe, sortino | The excess mean over volatility, or over downside | threshold |
omega, gain_to_pain | Gains over losses around the threshold, or their sum over the losses | threshold |
kappa | The excess mean over the root of the lower partial moment of order | threshold, order |
var, cvar | The loss at the alpha quantile, and the mean loss past it | alpha |
starr, rachev | The excess mean over CVaR, and the gain tail's mean over the loss tail's | threshold, alpha |
max_drawdown, cdar, ulcer | The deepest drawdown, the mean of the worst 1 - alpha of them, and their root mean square | compound; alpha for cdar |
calmar, martin | The excess mean over the deepest drawdown, or over the Ulcer index | threshold, compound |
beta, alpha, treynor | Against a benchmark, at the threshold | benchmark, threshold |
tracking_error, information | The active return's volatility, and its mean over that | benchmark |
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:
| Function | Returns |
|---|---|
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.
| Method | Finds |
|---|---|
equal, inverse_vol | Equal weights, or weights in proportion to 1 / volatility. |
risk_parity | Equal risk contributions, or the shares in budgets. |
hrp | Hierarchical risk parity. |
min_variance (or vol) | The least variance. |
utility | The largest mean less half risk_aversion times the variance. |
sharpe, information, diversification | The largest ratio. |
mean, alpha | The largest score. |
beta, treynor | The lowest absolute beta, or the largest Treynor ratio. |
omega, gain_to_pain | The largest ratio. Both give the same weights, since Omega is 1 plus gain to pain. |
cvar, cdar, max_drawdown, downside, ulcer | The least of it. |
sortino, kappa, starr, calmar, martin, rachev | The 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.
| Option | What it does |
|---|---|
lower, upper | Bounds on each weight: one number, or one per name. A negative lower allows shorts. |
gross | A cap on the sum of absolute weights. |
groups, group_lower, group_upper | One group number per name, and bounds on each group's total. |
held | The 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_turnover | A cap on the sum of absolute changes. |
turnover | A 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_return | On a risk word: the least risk with at least this mean. |
target_risk | On mean: the largest mean with at most this risk. |
threshold, alpha, benchmark, order, risk_aversion, estimator | The measure's own inputs. |
mean, cov | Your own estimates, from any estimator, mpt.black_litterman included. |
budgets | Risk 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.status | Happens when | The weights |
|---|---|---|
solved | The optimum. | |
limits can't be met | The bounds, groups or a target_return rule out every mix. | All 0. |
no mix has a positive excess mean | A 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 reduce | The returns have no variance. | Equal for min_variance, risk_parity and hrp; where they started for the rest. |
target out of reach | target_risk is below the least risk there is. | The least-risk portfolio. |
singular: ties went to the most equal weights | The covariance is singular. | The most equal of the tied answers. |
stopped short | An 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
nnumbers a day. Push one value per name every day, na included, or every later day reads the wrong names.