Optimization using Optim and Roots

Hello, I am a new user of Julia and the Optim package, and I am looking for some guidance on a simple piece of code.

I have given my simple implementation of the equivalent of Excel XIRR (extended internal rate of return) function using both Optim and Roots packages. The implementation using Optim package is a lot faster than the Roots package (median time:32.300 μs (0.00% GC vs 113.100 μs (0.00% GC), but still feels slow when running it on a large cash flow or calling it in a loop for multiple calculations. I will appreciate, guidance on:

Question 1: In the Optim package, I had a to take the function to the power of 2, i.e.
result = optimize(x -> f(x)^2,0.0,1.0,Brent()) as otherwise, I was not getting sensible answers and Brent is the only method that seems to work on this function. Would someone be able to help me understand how I can use other algorithms in the Optim package as simply changing the name of algorithm does not work and why I have to take a function to the power of 2 in this case?

Question 2: My implementation of the xirr function is obviously basic, but I would like to understand if in terms of Julia code it is efficient or what I can do to make it faster to run. Again, will appreciate any guidance.

using Optim, Dates, DayCounts, Roots, BenchmarkTools
function xnpv(xirr,cf,dates)
    interval = map(d -> DayCounts.yearfrac(dates[1],d,DayCounts.Actual365Fixed()),dates)
    sum(cf./(1+xirr).^interval)
end
function xirr(xnpv,cf,dates)
    f(x) = xnpv(x,cf,dates)
    result = optimize(x -> f(x)^2,0.0,1.0,Brent())
    return result.minimizer
end
function xirr_roots(xnpv,cf,dates)
    f(x) = xnpv(x,cf,dates)
    return fzero(f,[0.0,1.0])
end
dates = Date(2012,12,31):Year(1):Date(2016,12,31)
cf = [-100,10,10,10,110]
@benchmark optimR = xirr(xnpv,cf,dates)
@benchmark Roots = xirr_roots(xnpv,cf,dates)

Run time (Optim and Roots)

It sounds like you’re trying to do root-seeking (finding x such that f(x) == 0) rather than optimization (finding x such that f(x) is minimized or maximized). For that I think you should be using Optim’s sister package NLSolve rather than Optim, but I am by no means an expert in this area.

2 Likes

What you did using Optim is exactly correct (although there are multiple ways to do it). Optim is a minimizer, not a root finder. So, you need to square the function so that its minimum is also its root. I use Optim in this fashion and it works well and is always fast (the Brent method is basically as optimized as it can get).

If you wanted to speed it up, you would need to look at optimizing your xnpv function.

This would make for a nice small package if something like this doesn’t exist already (IRR and related calculations), if you wanted to get your hands dirty learning how to make/publish packages as well.

1 Like

Thank you. That makes sense to me. I will try the NLsolve.jl package

Thanks for the explanation. This was exactly my question as how to optimize the xnpv function. Is it a matter of defining the types for variable inputs, (such as Float64 for cf)?

There is a package for basic IRR calculation, but I could not find anything for XIRR in Julia. As a newcomer to Julia, the basic difficulty with creating modules and/or packages is that it is very hard to figure out how to find a user defined module after you have defined it (most tutorials do not address this). I keep getting the error that package not found in the current path!

I will publish a package of this once I have figured out how to work with modules.

The first obvious thing I see is that you can move the interval computation outside of the objective function as it does not depend on the thing you are solving for. So I’d put that in xirr and change what you pass to xnpv. Adding type declarations will likely do nothing here.

Also if the IRR package that exists already is somewhat maintained, you might be better off just making a pull request to add this functionality to that package.

Making the change that you suggested has led to almost 10 fold reduction in the run time! Thank you

1 Like

@optimist Is there an optimised version of your XIRR script that you may share?

@askvorts Here is what I believe it to be an optimised version of the code. I’m fairly new to Julia, so there is likely to be scope for improvements.

This code will work only for positive XIRR in the range (0,1). If you expect any other values then you can simply change the bounds defined in result = optimize(x -> f(x)^2,0.0,1.0,Brent()) in the xirr function.

using Optim, Dates, DayCounts
function cf_freq(dates)
    map(d -> DayCounts.yearfrac(dates[1],d,DayCounts.Actual365Fixed()),dates)
end
function xnpv(xirr,cf,interval)
    sum(cf./(1+xirr).^interval)
end
function xirr(cf,dates)
    interval = cf_freq(dates)
    f(x) = xnpv(x,cf,interval)
    result = optimize(x -> f(x)^2,0.0,1.0,Brent())
    return result.minimizer
end

Run-time code:

dates = Date(2012,12,31):Year(1):Date(2016,12,31)
cf = [-100,10,10,10,110]
xirr(cf,dates)
1 Like

Thank you.