GoldfarbIdnaniSolver.jl

Goldfarb and Idnani quadratic programming solver
Author fabienlefloch
Popularity
5 Stars
Updated Last
3 Months Ago
Started In
November 2021

Julia Package GoldfarbIdnaniSolver

Status Coverage
Build Status codecov.io

Goldfarb and Idnani quadratic programming solver in native Julia.

This is a port of Berwin A. Turlach quadprog to the Julia language.

Motivation

Julia has access to several high quality convex optimizer, which are able to solve quadratic programming problems, such as, for example, COSMO. But those tend to be not optimized for pure basic quadratic problems. It supports dense as well as sparse matrices.

This solver can be 100 times faster on some problems (and without any specific code level optimization). Being native Julia, it also supports multiple precision big floats.

Usage

The solveQPcompact function implements the dual method of Goldfarb and Idnani (1982, 1983) for solving quadratic programming problems of the form min(-d^T b + 1/2 b^T D b) with the constraints A^T b >= b_0.

  • Create a sparse matrix with A, then transform it to the quadprog sparse format via

amat, aind = convertSparse(A)

  • and solve your problem

sol, lagr, crval, iact, nact, iter = solveQPcompact(D, d, amat, aind, b_0)

The solution is in sol. It is possible through factorized=true to skip the factorization step for further speed-up. In this case, the matrix D should contain its inverse square root. The parameter meq=0 allows to specify the number of equalities (as in quadprog R package).

The solveQP function is the same algorithm for dense matrices:

  • solve your problem using

sol, lagr, crval, iact, nact, iter = solveQP(D, d, A, b_0)

Example

A good example that shows the performance of the solver is to find the convex set closest to a set of financial option prices. This ensures the so-called arbitrage-free property of the option prices, see An arbitrage-free interpolation of class C2 for option prices.

In terms of code, we fill up the constraints in the sparse matrix G, and associated vector h. The D matrix is simply a diagonal matrix of squared weights, here we input the inverse square root directly. And then we call the GoldfarbIrdaniSolver

using GoldfarbIdnaniSolver, SparseArrays, LinearAlgebra
function filterConvexCallPrices(
   strikes::Vector{T},
   callPrices::Vector{T}, #undiscounted!
   weights::Vector{T},
   forward::T;
   tol = 1e-8
)::Tuple{Vector{T},Vector{T},Vector{T}} where {T}
   n = length(callPrices)
   G = spzeros(T, 2 * n, n)
   h = zeros(T, 2 * n)
   for i = 2:n-1
       dym = (strikes[i] - strikes[i-1])
       dy = (strikes[i+1] - strikes[i])
       G[i, i-1] = -1 / dym
       G[i, i] = 1 / dym + 1 / dy
       G[i, i+1] = -1 / dy
   end
   G[1, 1] = 1 / (strikes[2] - strikes[1])
   G[1, 2] = -G[1, 1]
   G[n, n] = 1 / (strikes[n] - strikes[n-1])
   G[n, n-1] = -G[n, n]
   for i = 1:n
       h[i] = -tol
       G[n+i, i] = -1
       h[n+i] = -max(forward - strikes[i], 0) - tol
   end
   h[1] = 1 - tol
   strikesf = strikes
   #call solver
   amat, aind = convertSparse(copy(-G'))
   factorized = true
   dmat = diagm(1.0 ./ weights)
   dvec = @. callPrices * weights^2
   nEqualities = (strikes[1] == 0) ? 1 : 0
   pricesf, lagr, crval, iact, nact, iter = solveQPcompact(dmat, dvec, amat, aind, -h, meq=nEqualities, factorized = true)
   return strikesf, pricesf, weights
end

In terms of Black-Scholes implied volatility, the convex filtering is very visible, and the implied volatility look smoother (linear interpolation is used in the figure): Implied volatilities

For the COSMO solver, the code below the comment would read

    W = spdiagm(weights)
    z = Variable(n)
    problem = minimize(square(norm(W * (z - callPrices))), G * z <= h)
    #solve!(problem, () -> SCS.Optimizer(verbose = 0))
    Convex.solve!(problem, () -> COSMO.Optimizer(verbose = false, eps_rel = 1e-8, eps_abs = 1e-8))
    pricesf = Convex.evaluate(z)
    return strikesf, pricesf, weights

We can benchmark the solvers as follows

using BenchmarkTools, StatsBase
strikes = Float64.([20, 25, 50, 55, 75, 100, 120, 125, 140, 150, 160, 175, 180, 195, 200, 210, 230, 240, 250, 255, 260, 270, 275, 280, 285, 290, 300, 310, 315, 320, 325, 330, 335, 340, 350, 360, 370, 380, 390, 400, 410, 420, 430, 440, 450, 460, 470, 480, 490, 500, 510, 520, 550, 580, 590, 600, 650, 670, 680, 690, 700])
prices = [337.9478782712897, 333.2264008151271, 310.49584376491333, 306.5789869909477, 288.3326474521462, 266.1436620149496, 249.44466994191146, 244.47045906634085, 232.02358238485036, 223.57092985061374, 215.47932505838847, 203.49665261662705, 199.45085022051444, 187.64870164975878, 183.73184487579312, 176.72338330960227, 161.59752782675685, 154.84695750485992, 147.27113520122242, 144.51478902657934, 141.11371474120148, 134.64682478802777, 131.65837649352014, 128.3346695814304, 125.60411253121669, 122.28040561912707, 115.9424612881003, 110.99712967626056, 108.21499437718802, 105.35549170482741, 102.75388027676055, 100.15226884869381, 97.60223566948576, 95.07799161470709, 90.23581650058495, 85.61989310557237, 80.97785070828226, 76.85159079958001, 72.72533089087777, 67.85118637372317, 64.96280443763155, 61.352327017517055, 58.025529966125816, 54.95662415902851, 51.810350978643, 49.07670378912775, 46.34305659961247, 43.11941604593882, 41.05628609158771, 38.735264892942695, 36.285298072150695, 34.8668962285343, 28.935397609774668, 24.370722585772782, 23.003898991015205, 21.27602765424615, 16.324515763803383, 14.545066178175517, 13.719814196435081, 13.049296961270962, 12.22404497953049]
weights = [0.18348804852853492, 0.14552754066163864, 0.06087460548064749, 0.05109612140267557, 0.03629649739986478, 0.025613847407177506, 0.020011216481149133, 0.019345036201403803, 0.01663883576809264, 0.015257762783202483, 0.014011545380885547, 0.012468184742920784, 0.012034729348791627, 0.010873040937928326, 0.010535157435308888, 0.009879102038500473, 0.008855540796164947, 0.00841084879707799, 0.008032374522333945, 0.007845822148042107, 0.00767850819280654, 0.00737017036267451, 0.007228761921893696, 0.007096391696183667, 0.00697143229125802, 0.006853569353455795, 0.006637272585167783, 0.0064509208672822085, 0.006365169495070308, 0.006284168069410054, 0.006209646991746033, 0.0061399098141406445, 0.0060750536990634395, 0.006014709985843373, 0.0059077251648834515, 0.0058176064495057645, 0.005741621638057057, 0.005682166059320373, 0.005635273659488148, 0.005598197720478721, 0.00557969512744412, 0.005570043788415912, 0.005571719928608071, 0.005583741283162423, 0.005606897345237971, 0.0056386186100743, 0.005680979257062336, 0.005742146253326264, 0.005798784362859748, 0.005870563857815834, 0.005958641403270033, 0.006029457615736634, 0.0063683433148160615, 0.0067726154457943945, 0.006927129393879053, 0.007136508419763993, 0.008060162361264996, 0.00852974277055327, 0.008785608144514201, 0.009023417426706495, 0.009332153986076881]
forward = 356.73063159822254
tte = 1.5917808219178082
strikesf, pricesf = filterConvexCallPrices(strikes, prices, weights, forward, tol = 1e-6)
rmse = rmsd(pricesf .* weights, prices .* weights)
@benchmark strikesf, pricesf = filterConvexCallPrices(strikes, prices, weights, forward, tol = 1e-6)

The benchmark results are:

Solver RMSE Time (ms)
GoldfarbIdnaniSolver 0.0031130349998388157 0.205
COSMO 0.0031130309597602370 21.485
SCS 0.0031130429769795193 8.381
ECOS 0.0031131478052242090 2.430

Used By Packages