Files
quantconnect--lean/Algorithm.Framework/Portfolio/MaximumSharpeRatioPortfolioOptimizer.cs
T
2026-07-13 13:02:50 +08:00

140 lines
6.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System.Collections.Generic;
using System.Linq;
using Accord.Math;
using Accord.Math.Optimization;
using Accord.Statistics;
namespace QuantConnect.Algorithm.Framework.Portfolio
{
/// <summary>
/// Provides an implementation of a portfolio optimizer that maximizes the portfolio Sharpe Ratio.
/// The interval of weights in optimization method can be changed based on the long-short algorithm.
/// The default model uses flat risk free rate and weight for an individual security range from -1 to 1.
/// </summary>
public class MaximumSharpeRatioPortfolioOptimizer : IPortfolioOptimizer
{
private double _lower;
private double _upper;
private double _riskFreeRate;
/// <summary>
/// Initialize a new instance of <see cref="MaximumSharpeRatioPortfolioOptimizer"/>
/// </summary>
/// <param name="lower">Lower constraint</param>
/// <param name="upper">Upper constraint</param>
/// <param name="riskFreeRate"></param>
public MaximumSharpeRatioPortfolioOptimizer(double lower = -1, double upper = 1, double riskFreeRate = 0.0)
{
_lower = lower;
_upper = upper;
_riskFreeRate = riskFreeRate;
}
/// <summary>
/// Boundary constraints on weights: lw ≤ w ≤ up
/// </summary>
/// <remarks>
/// Expressed in the substituted variable y = κw (κ = 1ᵀy &gt; 0), the per-weight bounds
/// become linear: yᵢ up·(1ᵀy) ≤ 0 and yᵢ lw·(1ᵀy) ≥ 0.
/// </remarks>
/// <param name="size">number of variables</param>
/// <returns>enumeration of linear constraint objects</returns>
protected IEnumerable<LinearConstraint> GetBoundaryConditions(int size)
{
for (int i = 0; i < size; i++)
{
// yᵢ up·(1ᵀy) ≤ 0
var upper = Vector.Create(size, -_upper);
upper[i] += 1.0;
yield return new LinearConstraint(size)
{
CombinedAs = upper,
ShouldBe = ConstraintType.LesserThanOrEqualTo,
Value = 0.0
};
// yᵢ lw·(1ᵀy) ≥ 0
var lower = Vector.Create(size, -_lower);
lower[i] += 1.0;
yield return new LinearConstraint(size)
{
CombinedAs = lower,
ShouldBe = ConstraintType.GreaterThanOrEqualTo,
Value = 0.0
};
}
}
/// <summary>
/// Perform portfolio optimization for a provided matrix of historical returns and an array of expected returns
/// </summary>
/// <param name="historicalReturns">Matrix of annualized historical returns where each column represents a security and each row returns for the given date/time (size: K x N).</param>
/// <param name="expectedReturns">Array of double with the portfolio annualized expected returns (size: K x 1).</param>
/// <param name="covariance">Multi-dimensional array of double with the portfolio covariance of annualized returns (size: K x K).</param>
/// <returns>Array of double with the portfolio weights (size: K x 1)</returns>
public double[] Optimize(double[,] historicalReturns, double[] expectedReturns = null, double[,] covariance = null)
{
covariance = covariance ?? historicalReturns.Covariance();
var returns = (expectedReturns ?? historicalReturns.Mean(0)).Subtract(_riskFreeRate);
var size = covariance.GetLength(0);
var equalWeights = Vector.Create(size, 1.0 / size);
// The Charnes-Cooper substitution needs a portfolio with positive expected excess
// return to exist, otherwise the Sharpe ratio cannot be maximized.
var feasible = _lower >= 0 ? returns.Any(x => x > 0) : returns.Any(x => x != 0);
if (!feasible)
{
return equalWeights;
}
// Charnes-Cooper substitution y = κw (κ = 1ᵀy): maximizing the Sharpe ratio
// (µ r_f)ᵀw / √(wᵀΣw) becomes minimizing wᵀΣw subject to (µ r_f)ᵀy = 1,
// recovering the weights afterwards as w = y / (1ᵀy).
// https://quant.stackexchange.com/questions/18521/sharpe-maximization-under-quadratic-constraints
var constraints = new List<LinearConstraint>
{
// (µ r_f)ᵀy = 1
new LinearConstraint(size)
{
CombinedAs = returns,
ShouldBe = ConstraintType.EqualTo,
Value = 1.0
}
};
// lw ≤ w ≤ up
constraints.AddRange(GetBoundaryConditions(size));
// Setup solver: minimize yᵀΣy
var optfunc = new QuadraticObjectiveFunction(covariance, Vector.Create(size, 0.0));
var solver = new GoldfarbIdnani(optfunc, constraints);
// Solve problem
var success = solver.Minimize(Vector.Copy(equalWeights));
if (!success)
{
return equalWeights;
}
// Recover the portfolio weights: w = y / (1ᵀy)
var y = solver.Solution;
var sum = y.Sum();
return sum > 0 ? y.Divide(sum) : equalWeights;
}
}
}