diff --git a/Algorithm.Framework/Portfolio/MaximumSharpeRatioPortfolioOptimizer.cs b/Algorithm.Framework/Portfolio/MaximumSharpeRatioPortfolioOptimizer.cs
index 30ffeed3885f..abad946fdee4 100644
--- a/Algorithm.Framework/Portfolio/MaximumSharpeRatioPortfolioOptimizer.cs
+++ b/Algorithm.Framework/Portfolio/MaximumSharpeRatioPortfolioOptimizer.cs
@@ -14,6 +14,7 @@
*/
using System.Collections.Generic;
+using System.Linq;
using Accord.Math;
using Accord.Math.Optimization;
using Accord.Statistics;
@@ -44,41 +45,36 @@ public MaximumSharpeRatioPortfolioOptimizer(double lower = -1, double upper = 1,
_riskFreeRate = riskFreeRate;
}
- ///
- /// Sum of all weight is one: 1^T w = 1 / Σw = 1
- ///
- /// number of variables
- /// linear constraint object
- protected LinearConstraint GetBudgetConstraint(int size)
- {
- return new LinearConstraint(size)
- {
- CombinedAs = Vector.Create(size, 1.0),
- ShouldBe = ConstraintType.EqualTo,
- Value = 1.0
- };
- }
-
///
/// Boundary constraints on weights: lw ≤ w ≤ up
///
+ ///
+ /// Expressed in the substituted variable y = κw (κ = 1ᵀy > 0), the per-weight bounds
+ /// become linear: yᵢ − up·(1ᵀy) ≤ 0 and yᵢ − lw·(1ᵀy) ≥ 0.
+ ///
/// number of variables
/// enumeration of linear constraint objects
protected IEnumerable GetBoundaryConditions(int size)
{
for (int i = 0; i < size; i++)
{
- yield return new LinearConstraint(1)
+ // yᵢ − up·(1ᵀy) ≤ 0
+ var upper = Vector.Create(size, -_upper);
+ upper[i] += 1.0;
+ yield return new LinearConstraint(size)
{
- VariablesAtIndices = new int[] { i },
- ShouldBe = ConstraintType.GreaterThanOrEqualTo,
- Value = _lower
+ CombinedAs = upper,
+ ShouldBe = ConstraintType.LesserThanOrEqualTo,
+ Value = 0.0
};
- yield return new LinearConstraint(1)
+ // yᵢ − lw·(1ᵀy) ≥ 0
+ var lower = Vector.Create(size, -_lower);
+ lower[i] += 1.0;
+ yield return new LinearConstraint(size)
{
- VariablesAtIndices = new int[] { i },
- ShouldBe = ConstraintType.LesserThanOrEqualTo,
- Value = _upper
+ CombinedAs = lower,
+ ShouldBe = ConstraintType.GreaterThanOrEqualTo,
+ Value = 0.0
};
}
}
@@ -96,36 +92,49 @@ public double[] Optimize(double[,] historicalReturns, double[] expectedReturns =
var returns = (expectedReturns ?? historicalReturns.Mean(0)).Subtract(_riskFreeRate);
var size = covariance.GetLength(0);
- var x0 = Vector.Create(size, 1.0 / size);
- var k = returns.Dot(x0);
+ 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
{
- // Sharpe Maximization under Quadratic Constraints
- // https://quant.stackexchange.com/questions/18521/sharpe-maximization-under-quadratic-constraints
- // (µ − r_f)^T w = k
+ // (µ − r_f)ᵀy = 1
new LinearConstraint(size)
{
CombinedAs = returns,
ShouldBe = ConstraintType.EqualTo,
- Value = k
+ Value = 1.0
}
};
- // Σw = 1
- constraints.Add(GetBudgetConstraint(size));
-
// lw ≤ w ≤ up
constraints.AddRange(GetBoundaryConditions(size));
- // Setup solver
+ // 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(x0));
- var sharpeRatio = returns.Dot(solver.Solution) / solver.Value;
- return success ? solver.Solution : x0;
+ 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;
}
}
}
\ No newline at end of file
diff --git a/Algorithm.Framework/Portfolio/MaximumSharpeRatioPortfolioOptimizer.py b/Algorithm.Framework/Portfolio/MaximumSharpeRatioPortfolioOptimizer.py
index 14a7ee8d99d8..0e6246859e4d 100644
--- a/Algorithm.Framework/Portfolio/MaximumSharpeRatioPortfolioOptimizer.py
+++ b/Algorithm.Framework/Portfolio/MaximumSharpeRatioPortfolioOptimizer.py
@@ -55,24 +55,23 @@ def optimize(self, historical_returns, expected_returns = None, covariance = Non
size = covariance.columns.size # K x 1
x0 = np.array(size * [1. / size])
- k = expected_returns.dot(x0)
- # Sharpe Maximization under Quadratic Constraints
+ # SLSQP maximizes the Sharpe ratio (µ − r_f)^T w / √(w^T Σ w) directly, so the fractional
+ # objective is optimized in place without any substitution. The budget constraint Σw = 1 and
+ # the per-weight bounds lw ≤ w ≤ up are applied as-is. The previous implementation instead
+ # fixed (µ − r_f)^T w to the equal-weight return, which collapsed the optimizer to minimum
+ # variance. The C# implementation uses the Charnes-Cooper QP substitution because its solver
+ # only handles quadratic objectives.
# https://quant.stackexchange.com/questions/18521/sharpe-maximization-under-quadratic-constraints
- # (µ − r_f)^T w = k
constraints = [
- {'type': 'eq', 'fun': lambda weights: expected_returns.dot(weights) - k}]
+ # Σw = 1
+ {'type': 'eq', 'fun': lambda weights: self.get_budget_constraint(weights)}]
- # Σw = 1
- constraints.append(
- {'type': 'eq', 'fun': lambda weights: self.get_budget_constraint(weights)})
-
- opt = minimize(lambda weights: self.portfolio_variance(weights, covariance), # Objective function
+ opt = minimize(lambda weights: -expected_returns.dot(weights) / np.sqrt(self.portfolio_variance(weights, covariance)), # Objective function: −Sharpe ratio
x0, # Initial guess
bounds = self.get_boundary_conditions(size), # Bounds for variables: lw ≤ w ≤ up
constraints = constraints, # Constraints definition
method='SLSQP') # Optimization method: Sequential Least SQuares Programming
- sharpe_ratio = expected_returns.dot(opt['x']) / opt.fun
return opt['x'] if opt['success'] else x0
diff --git a/Tests/Algorithm/Framework/Portfolio/MaximumSharpeRatioPortfolioOptimizerTests.cs b/Tests/Algorithm/Framework/Portfolio/MaximumSharpeRatioPortfolioOptimizerTests.cs
index 5efd3e04f114..dceb7272128e 100644
--- a/Tests/Algorithm/Framework/Portfolio/MaximumSharpeRatioPortfolioOptimizerTests.cs
+++ b/Tests/Algorithm/Framework/Portfolio/MaximumSharpeRatioPortfolioOptimizerTests.cs
@@ -65,14 +65,14 @@ public void SetUp()
ExpectedResults = new List
{
- new double[] { -0.562396, 0.608942, 0.953453 },
- new double[] { 0.686025, -0.269589, 0.583023 },
- new double[] { 0.26394, -0.043374, 0.779434 },
- new double[] { -0.223905, 0.401036, 1, 0.065329, -0.24246 },
- new double[] { 0.5, 0.5 },
new double[] { -0.5, 0.5, 1 },
- new double[] { -0.242647, 1, 0.242647 },
- new double[] { -1, 0.922902, 0.364512, 0.712585 },
+ new double[] { 0, 0, 1 },
+ new double[] { -0.404692, 0.404692, 1 },
+ new double[] { -0.418338, 0.023261, 1, 0.040668, 0.35441 },
+ new double[] { 0.5, 0.5 },
+ new double[] { -0.670213, 0.670213, 1 },
+ new double[] { -1, 1, 1 },
+ new double[] { -1, 0.315476, 0.684524, 1 },
};
}
@@ -98,7 +98,7 @@ public override void OptimizeWeightings(int testCaseNumber)
public void OptimizeWeightingsSpecifyingLowerBoundAndRiskFreeRate(int testCaseNumber)
{
var testOptimizer = new MaximumSharpeRatioPortfolioOptimizer(lower: 0, riskFreeRate: 0.04);
- var expectedResult = new double[] { 0, 0.44898, 0.55102 };
+ var expectedResult = new double[] { 0, 0, 1 };
var result = testOptimizer.Optimize(HistoricalReturns[testCaseNumber]);
@@ -106,13 +106,14 @@ public void OptimizeWeightingsSpecifyingLowerBoundAndRiskFreeRate(int testCaseNu
}
[Test]
- public void SingleSecurityPortfolioReturnsNaN()
+ public void SingleSecurityPortfolioReturnsFullWeight()
{
var testOptimizer = new MaximumSharpeRatioPortfolioOptimizer();
var historicalReturns = new double[,] { { -0.1 } };
var expectedReturns = new double[] { -0.1 };
- var expectedResult = new double[] { double.NaN };
+ // With a single security the budget constraint Σw = 1 leaves it fully invested
+ var expectedResult = new double[] { 1 };
var result = testOptimizer.Optimize(historicalReturns, expectedReturns);
@@ -151,5 +152,73 @@ public void BoundariesAreNotViolated()
Assert.LessOrEqual(rounded, upper);
};
}
+
+ // Cases with a positive-definite covariance, where the maximum Sharpe ratio
+ // portfolio is well defined (case 1 has a zero-variance asset and case 4 an
+ // indefinite covariance, so they are excluded here).
+ [TestCase(0)]
+ [TestCase(2)]
+ [TestCase(5)]
+ [TestCase(7)]
+ public void OptimizedWeightsMaximizeSharpeRatio(int testCaseNumber)
+ {
+ // Independent of the hardcoded ExpectedResults: the returned portfolio must
+ // achieve a Sharpe ratio no lower than equal weights or any other feasible
+ // portfolio drawn from the constraint set.
+ var testOptimizer = new MaximumSharpeRatioPortfolioOptimizer();
+ var expectedReturns = ExpectedReturns[testCaseNumber];
+ var covariance = Covariances[testCaseNumber];
+
+ var result = testOptimizer.Optimize(HistoricalReturns[testCaseNumber], expectedReturns, covariance);
+ var optimalSharpe = SharpeRatio(result, expectedReturns, covariance);
+
+ var size = result.Length;
+ var equalWeights = Enumerable.Repeat(1.0 / size, size).ToArray();
+ Assert.GreaterOrEqual(optimalSharpe, SharpeRatio(equalWeights, expectedReturns, covariance));
+
+ var random = new Random(0);
+ for (var i = 0; i < 10000; i++)
+ {
+ var candidate = RandomFeasibleWeights(random, size, lower: -1.0, upper: 1.0);
+ Assert.GreaterOrEqual(optimalSharpe + 1e-6, SharpeRatio(candidate, expectedReturns, covariance));
+ }
+ }
+
+ private static double SharpeRatio(double[] weights, double[] expectedReturns, double[,] covariance)
+ {
+ var size = weights.Length;
+ var portfolioReturn = 0.0;
+ var portfolioVariance = 0.0;
+ for (var i = 0; i < size; i++)
+ {
+ portfolioReturn += weights[i] * expectedReturns[i];
+ for (var j = 0; j < size; j++)
+ {
+ portfolioVariance += weights[i] * covariance[i, j] * weights[j];
+ }
+ }
+ return portfolioReturn / Math.Sqrt(portfolioVariance);
+ }
+
+ private static double[] RandomFeasibleWeights(Random random, int size, double lower, double upper)
+ {
+ // Draw weights uniformly from the box and keep only those summing to one.
+ while (true)
+ {
+ var weights = new double[size];
+ var sum = 0.0;
+ for (var i = 0; i < size - 1; i++)
+ {
+ weights[i] = lower + random.NextDouble() * (upper - lower);
+ sum += weights[i];
+ }
+ var last = 1.0 - sum;
+ if (last >= lower && last <= upper)
+ {
+ weights[size - 1] = last;
+ return weights;
+ }
+ }
+ }
}
}