Skip to content

Avoid exponential overflow in entropic risk calculations - #270

Closed
galafis wants to merge 1 commit into
dcajasn:masterfrom
galafis:fix/entropic-risk-stability
Closed

galafis wants to merge 1 commit into
dcajasn:masterfrom
galafis:fix/entropic-risk-stability

Conversation

@galafis

@galafis galafis commented Oct 24, 2026

Copy link
Copy Markdown

Entropic_RM and the _Entropic_RM objective used by EVaR_Hist compute log(mean(exp(-X/z))) directly. Valid finite inputs overflow or underflow when |X/z| is large: a constant return of -1 at z=.001 produces infinity instead of the finite result 1-.001*log(.05). Positive constant returns underflow to zero inside the logarithm.

Evaluate the same expression with scipy.special.logsumexp, subtracting log(T) and log(alpha), in both paths. No risk-measure definition or optimizer choice changes.

Eight regression cases cover positive/negative constants, mixed returns and ordinary values in the public function and optimizer callback. Constant and mixed-case expectations use independent closed forms. Before: six fail on overflow or log(0), two ordinary controls pass. After: eight pass. git diff --check passes.

Related historical report: #51; the failure is reproducible on current NumPy 2.5.4/SciPy 1.18.1, Python 3.12.14. SciPy documents logsumexp as the numerically stable log of a sum of exponentials: https://docs.scipy.org/doc/scipy/reference/generated/scipy.special.logsumexp.html .

Validation uses current Python source plus the same-version 7.4.0 wheel's compiled extension and generated version metadata. The full optimizer/golden-file suite was not run.

@arhancanli

Copy link
Copy Markdown

I checked the new formula against the old one outside the library (numpy and scipy, same expressions as the diff, a shaped (T, 1) like the .item() path expects):

case old new
250 normal daily returns, z = 0.5 1.4984350445498946 1.4984350445498948
same, z = 0.05 0.14982853027500725 0.14982853027500725
same, z = 0.001 0.03247209624223109 0.032472096242231095
4 returns all -1.0, z = 0.001 inf 1.002995732273554
250 normal returns plus one -100% day, z = 0.001 inf 0.9974742713556919

So the two agree to the last digit or two where the old one is finite, and the new one stays finite where np.exp(-a / z) overflows. The old version breaks once -a / z exceeds about 709.78, i.e. any return below -709.78 * z: with z = 0.001 a constant -0.7 is still finite and -0.8 gives inf, which is why the -0.5/-0.7/-0.8 boundary shows up so soon at small z. That matters for _Entropic_RM(z, X, alpha) because z is the variable minimize searches over, so the optimiser can step to a small z where the old objective returned inf or nan.

One edge I would add a test for: a zero-row input. logsumexp of an empty axis is -inf and np.log(0) is -inf, so the new expression returns nan (I ran it); it is also nan on the old path, so this is not a regression, but the raise-on-non-finite check above it could cover len(a) == 0 explicitly.

@dcajasn

dcajasn commented Oct 29, 2026

Copy link
Copy Markdown
Owner

Hi @galafis,

I am going to reject this pull request, as all the errors occur in scenarios that are not realistic. It is actually a good thing that the function failed when faced with a returns vector that do not correspond to reality. If there is a 100% loss (Not possible in finance), all returns are the same, or a sample is too small, the problem is the data. Please read about financial theory before submitting a pull request.

Best,
Dany

@dcajasn dcajasn closed this Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants