
org.ojalgo.finance.portfolio.MarkowitzModel Maven / Gradle / Ivy
Show all versions of ojalgo-finance Show documentation
/*
* Copyright 1997-2022 Optimatika
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.ojalgo.finance.portfolio;
import static org.ojalgo.function.constant.BigMath.*;
import java.math.BigDecimal;
import java.util.HashMap;
import org.ojalgo.function.constant.PrimitiveMath;
import org.ojalgo.matrix.Primitive64Matrix;
import org.ojalgo.netio.BasicLogger;
import org.ojalgo.optimisation.ExpressionsBasedModel;
import org.ojalgo.optimisation.Optimisation;
import org.ojalgo.scalar.Scalar;
import org.ojalgo.structure.Access1D;
import org.ojalgo.type.context.NumberContext;
/**
*
* The Markowitz model, in this class, is defined as:
*
*
* min (RAF/2) [w]T[C][w] - [w]T[r]
* subject to |[w]| = 1
*
*
* RAF stands for Risk Aversion Factor. Instead of specifying a desired risk or return level you specify a
* level of risk aversion that is used to balance the risk and return.
*
*
* The expected returns for each of the assets must be excess returns. Otherwise this formulation is wrong.
*
*
* The total weights of all assets will always be 100%, but shorting can be allowed or not according to your
* preference. ( {@linkplain #setShortingAllowed(boolean)} ) In addition you may set lower and upper limits on
* any individual asset. ( {@linkplain #setLowerLimit(int, BigDecimal)} and
* {@linkplain #setUpperLimit(int, BigDecimal)} )
*
*
* Risk-free asset: That means there is no excess return and zero variance. Don't (try to) include a risk-free
* asset here.
*
*
* Do not worry about the minus sign in front of the return part of the objective function - it is
* handled/negated for you. When you're asked to supply the expected excess returns you should supply
* precisely that.
*
*
* Basic usage instructions
*
* After you've instantiated the MarkowitzModel you need to do one of three different things:
*
* - {@link #setRiskAversion(Number)} unless this was already set in the {@link MarketEquilibrium} or
* {@link FinancePortfolio.Context} used to instantiate the MarkowitzModel
* - {@link #setTargetReturn(BigDecimal)}
* - {@link #setTargetVariance(BigDecimal)}
*
*
* Optionally you may {@linkplain #setLowerLimit(int, BigDecimal)},
* {@linkplain #setUpperLimit(int, BigDecimal)} or {@linkplain #setShortingAllowed(boolean)}.
*
*
* To get the optimal asset weighs you simply call {@link #getWeights()} or {@link #getAssetWeights()}.
*
*
* If the results are not what you expect the first thing you should try is to turn on optimisation model
* validation: model.optimisation().validate(true);
*
*
* @author apete
*/
public final class MarkowitzModel extends OptimisedPortfolio {
private static final double _0_0 = ZERO.doubleValue();
private static final double INIT = PrimitiveMath.SQRT.invoke(PrimitiveMath.TEN);
private static final double MAX = PrimitiveMath.HUNDRED * PrimitiveMath.HUNDRED;
private static final double MIN = PrimitiveMath.HUNDREDTH;
private static final NumberContext TARGET_CONTEXT = NumberContext.getGeneral(5, 4);
private final HashMap myConstraints = new HashMap<>();
private transient ExpressionsBasedModel myOptimisationModel;
private BigDecimal myTargetReturn;
private BigDecimal myTargetVariance;
public MarkowitzModel(final FinancePortfolio.Context portfolioContext) {
super(portfolioContext);
}
public MarkowitzModel(final MarketEquilibrium marketEquilibrium, final Primitive64Matrix expectedExcessReturns) {
super(marketEquilibrium, expectedExcessReturns);
}
public MarkowitzModel(final Primitive64Matrix covarianceMatrix, final Primitive64Matrix expectedExcessReturns) {
super(covarianceMatrix, expectedExcessReturns);
}
/**
* Will add a constraint on the sum of the asset weights specified by the asset indices. Either (but not
* both) of the limits may be null.
*/
public LowerUpper addConstraint(final BigDecimal lowerLimit, final BigDecimal upperLimit, final int... assetIndeces) {
return myConstraints.put(assetIndeces, new LowerUpper(lowerLimit, upperLimit));
}
public void clearAllConstraints() {
myConstraints.clear();
this.reset();
}
public void setLowerLimit(final int assetIndex, final BigDecimal lowerLimit) {
this.getVariable(assetIndex).lower(lowerLimit);
this.reset();
}
/**
*
* Will set the target return to whatever you input and the target variance to null
.
*
*
* Setting the target return implies that you disregard the risk aversion factor and want the minimum risk
* portfolio with return that is equal to or as close to the target as possible.
*
*
* There is a performance penalty for setting a target return as the underlying optimisation model has to
* be solved several (many) times with different pararmeters (different risk aversion factors).
*
*
* Setting a target return (or variance) is not recommnded. It's much better to simply modify the risk
* aversion factor.
*
*
* @see #setTargetVariance(BigDecimal)
*/
public void setTargetReturn(final BigDecimal targetReturn) {
myTargetReturn = targetReturn;
myTargetVariance = null;
this.reset();
}
/**
*
* Will set the target variance to whatever you input and the target return to null
.
*
*
* Setting the target variance implies that you disregard the risk aversion factor and want the maximum
* return portfolio with risk that is equal to or as close to the target as possible.
*
*
* There is a performance penalty for setting a target variance as the underlying optimisation model has
* to be solved several (many) times with different pararmeters (different risk aversion factors).
*
*
* Setting a target variance is not recommnded. It's much better to modify the risk aversion factor.
*
*
* @see #setTargetReturn(BigDecimal)
*/
public void setTargetVariance(final BigDecimal targetVariance) {
myTargetVariance = targetVariance;
myTargetReturn = null;
this.reset();
}
public void setUpperLimit(final int assetIndex, final BigDecimal upperLimit) {
this.getVariable(assetIndex).upper(upperLimit);
this.reset();
}
@Override
public String toString() {
if (myOptimisationModel == null) {
this.calculateAssetWeights();
}
return myOptimisationModel.toString();
}
private ExpressionsBasedModel generateOptimisationModel(final double riskAversion) {
if (myOptimisationModel == null) {
myOptimisationModel = this.makeModel(myConstraints);
}
myOptimisationModel.getExpression(VARIANCE).weight(riskAversion / 2.0);
if (this.getOptimisationOptions().logger_appender != null) {
BasicLogger.debug();
BasicLogger.debug("@@@@@@@@@@@");
BasicLogger.debug("Iteration RAF: {}", riskAversion);
BasicLogger.debug("Iteration point: {}", myOptimisationModel.getVariableValues());
BasicLogger.debug("@@@@@@@@@@@");
BasicLogger.debug();
}
return myOptimisationModel;
}
/**
* Constrained optimisation.
*/
@Override
protected Primitive64Matrix calculateAssetWeights() {
if (this.getOptimisationOptions().logger_appender != null) {
BasicLogger.debug();
BasicLogger.debug("###################################################");
BasicLogger.debug("BEGIN RAF: {} MarkowitzModel optimisation", this.getRiskAversion());
BasicLogger.debug("###################################################");
BasicLogger.debug();
}
Optimisation.Result tmpResult;
if ((myTargetReturn != null) || (myTargetVariance != null)) {
final double tmpTargetValue;
if (myTargetVariance != null) {
tmpTargetValue = myTargetVariance.doubleValue();
} else if (myTargetReturn != null) {
tmpTargetValue = myTargetReturn.doubleValue();
} else {
tmpTargetValue = _0_0;
}
tmpResult = this.generateOptimisationModel(_0_0).minimise();
double tmpTargetNow = _0_0;
double tmpTargetDiff = _0_0;
double tmpTargetLast = _0_0;
if (tmpResult.getState().isFeasible()) {
double tmpCurrent;
double tmpLow;
double tmpHigh;
if (this.isDefaultRiskAversion()) {
tmpCurrent = INIT;
tmpLow = MAX;
tmpHigh = MIN;
} else {
tmpCurrent = this.getRiskAversion().doubleValue();
tmpLow = tmpCurrent * INIT;
tmpHigh = tmpCurrent / INIT;
}
do {
final ExpressionsBasedModel tmpModel = this.generateOptimisationModel(tmpCurrent);
tmpResult = tmpModel.minimise();
tmpTargetLast = tmpTargetNow;
if (myTargetVariance != null) {
tmpTargetNow = this.calculatePortfolioVariance(tmpResult).doubleValue();
} else if (myTargetReturn != null) {
tmpTargetNow = this.calculatePortfolioReturn(tmpResult, this.calculateAssetReturns()).doubleValue();
} else {
tmpTargetNow = tmpTargetValue;
}
tmpTargetDiff = tmpTargetNow - tmpTargetValue;
if (this.getOptimisationOptions().logger_appender != null) {
BasicLogger.debug();
BasicLogger.debug("RAF: {}", tmpCurrent);
BasicLogger.debug("Last: {}", tmpTargetLast);
BasicLogger.debug("Now: {}", tmpTargetNow);
BasicLogger.debug("Target: {}", tmpTargetValue);
BasicLogger.debug("Diff: {}", tmpTargetDiff);
BasicLogger.debug("Iteration: {}", tmpResult);
BasicLogger.debug();
}
if (tmpTargetDiff < _0_0) {
tmpLow = tmpCurrent;
} else if (tmpTargetDiff > _0_0) {
tmpHigh = tmpCurrent;
}
tmpCurrent = PrimitiveMath.SQRT.invoke(tmpLow * tmpHigh);
} while (!TARGET_CONTEXT.isSmall(tmpTargetValue, tmpTargetDiff) && TARGET_CONTEXT.isDifferent(tmpHigh, tmpLow));
}
} else {
tmpResult = this.generateOptimisationModel(this.getRiskAversion().doubleValue()).minimise();
}
return this.handle(tmpResult);
}
@Override
protected void reset() {
super.reset();
myOptimisationModel = null;
}
Scalar> calculatePortfolioReturn(final Access1D> weightsVctr, final Primitive64Matrix returnsVctr) {
return super.calculatePortfolioReturn(MATRIX_FACTORY.columns(weightsVctr), returnsVctr);
}
Scalar> calculatePortfolioVariance(final Access1D> weightsVctr) {
return super.calculatePortfolioVariance(MATRIX_FACTORY.columns(weightsVctr));
}
}