JuMP Non-Linear Optimization

@odow,
Thanks for helping me with the optimization problem earlier (Feb 3). Indeed thank you for JuMP in the very first place. It’s a great package.

JuMP.jl is a dependency for PortoflioAnalytics.jl which I am actively developing. I will soon make a second release and want to improve the portfolio optimization function in it and wondering if you could make a sanity check for the below function.

Function either minimizes the portfolio variance or maximizes the Sharpe ratio. I want users to define (if they want) a target portfolio mean return so that they can move across the efficient frontier.

Sorry for the ugly pictures!

The output looks reasonable, but I am unsure if it is doing what I think it is.

I am placing the code here - I hope it will be easy to reproduce.

using JuMP
using Ipopt
using Statistics
using TSFrames
using NamedArrays

bond = [0.06276629, 0.03958098, 0.08456482,0.02759821,0.09584956,0.06363253,0.02874502,0.02707264,0.08776449,0.02950032]
stock = [0.1759782,0.20386651,0.21993588,0.3090001,0.17365969,0.10465274,0.07888138,0.13220847,0.28409742,0.14343067]

R = TSFrame([bond stock], colnames  = [:bond, :stock])


function PortfolioOptimize(R::TSFrame, objective::String = "minumum variance"; target = Nothing, Rf::Number = 0)
    

    means = mean.(eachcol(Matrix(R)))
    covMatrix = cov(Matrix(R))
    
    model = Model(Ipopt.Optimizer)
    # set_silent(model)

    @variable(model, 0 <= w[1:size(R)[2]] <= 1)
    @constraint(model, sum(w) == 1)
    
    
    if objective == "minumum variance"
        @expression(model, quad_expr, w' * covMatrix * w)
        @NLobjective(model, Min, sqrt(quad_expr))
        if target != Nothing
            @NLconstraint(model, sum(w[i] * means[i] for i in 1:size(R)[2]) >= target)
        end
    elseif objective == "maximum sharpe"
        
        # Equation should be transformed to either scalar values or expressed in the form of expressions. 
        # N = size(covMatrix, 1) # either 
        # @NLobjective(model, Max, (sum(w[i] * means[i] for i in 1:N) - Rf) / sqrt(sum(w[i] * covMatrix[i, j] * w[j] for i in 1:N, j in 1:N))) # either
        @expression(model, quad_expr1, w' * means) # or
        @expression(model, quad_expr2, w' * covMatrix * w) # or
        @NLobjective(model, Max, (quad_expr1 - Rf)/sqrt(quad_expr2))
        if target != Nothing
            # @NLconstraint(model, sum(w[i] * means[i] for i in 1:size(R)[2]) >= target) # either
            @NLconstraint(model, quad_expr1 >= target) # or
        end
    else
        throw(ArgumentError("one of the available objective must be chosen"))
    end

    
    optimize!(model)

    portobj = objective_value(model)
    
    weights = value.(w)
    
    pmreturn = weights' * means

    colnames = names(R) # used only for naming array
    weights = NamedArray(weights, colnames, "Optimal Weights")


    return (preturn = pmreturn, obj = portobj, pweights = weights)
end


opt1 = PortfolioOptimize(R, "minumum variance", target = 0.1)
opt2 = PortfolioOptimize(R, "maximum sharpe", target = 0.15)