This is an automated email from the ASF dual-hosted git repository.

robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git

commit 6433ce58f4697ae155b72d9d9566ba0589be03b4
Author: Robert Lazarski <[email protected]>
AuthorDate: Thu May 7 10:30:38 2026 -1000

    Add Merton jump-diffusion and fix variance/percentile estimators
    
    Parity with Axis2/C commit 6e577eb + review fixes fb40242, 0b281fd.
    
    Merton (1976) jump-diffusion model:
      - New request fields: model ("gbm"|"merton"), jumpIntensity,
        jumpMean, jumpVol — all with defaults matching C implementation.
      - Drift correction: (μ − σ²/2 − λk)·dt preserves E[S(T)].
      - Bernoulli jump process with lambda·dt validation (> 0.1 rejected).
      - Response includes "model" field echoing which model was used.
    
    Numerical stability (from Gemini quant review):
      - Replaced one-pass variance (sumSq/N − mean²) with two-pass
        algorithm. The one-pass formula suffers catastrophic cancellation
        when stdDev << mean.
      - Fixed VaR percentile indexing: ceil(p·N) − 1 instead of
        floor(p·N). The floor estimator selects one observation too far
        from the tail, systematically understating VaR.
    
    All existing tests pass.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---
 .../webservices/FinancialBenchmarkService.java     | 76 ++++++++++++++++++----
 .../springboot/webservices/MonteCarloRequest.java  | 55 +++++++++++++++-
 .../springboot/webservices/MonteCarloResponse.java |  6 ++
 3 files changed, 122 insertions(+), 15 deletions(-)

diff --git 
a/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/FinancialBenchmarkService.java
 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/FinancialBenchmarkService.java
index 4422b1eb69..3f3fccb14c 100644
--- 
a/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/FinancialBenchmarkService.java
+++ 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/FinancialBenchmarkService.java
@@ -365,15 +365,43 @@ public class FinancialBenchmarkService {
             throw JsonRpcFaultException.validationError("volatility must be >= 
0.");
         }
 
-        // ── Pre-computed GBM constants 
────────────────────────────────────────
+        // ── Model selection 
────────────────────────────────────────────────────
+        boolean isMerton = "merton".equalsIgnoreCase(request.getModel());
+        double jumpIntensity = request.getJumpIntensity();
+        double jumpMean = request.getJumpMean();
+        double jumpVol = request.getJumpVol();
+
+        if (isMerton && jumpVol < 0.0) {
+            throw JsonRpcFaultException.validationError("jumpVol must be >= 
0.");
+        }
+
+        // ── Pre-computed constants 
────────────────────────────────────────────
         // dt         — length of one time step in years (e.g., 1/252 for one
         //              trading day when nPeriodsPerYear = 252)
-        // drift      — (μ − σ²/2)·dt.  The (−σ²/2) term is the Itô correction
-        //              that keeps E[S(T)] = S(0)·exp(μ·T).  Do not drop it.
+        // drift      — (μ − σ²/2 − λk)·dt for Merton, (μ − σ²/2)·dt for GBM.
+        //              The −λk term compensates for the expected jump so that
+        //              E[S(T)] = S(0)·exp(μ·T) regardless of jump parameters.
         // volSqrtDt  — σ·√dt.  Scales each standard-normal shock by the
         //              one-step standard deviation.
         double dt = 1.0 / npy;
-        double drift = (mu - 0.5 * sigma * sigma) * dt;
+        double jumpCompensation = 0.0;
+        double jumpLambdaDt = 0.0;
+        if (isMerton) {
+            double k = Math.exp(jumpMean + 0.5 * jumpVol * jumpVol) - 1.0;
+            jumpCompensation = jumpIntensity * k;
+            jumpLambdaDt = jumpIntensity * dt;
+            // The Bernoulli approximation for a Poisson process is only valid
+            // when λ·dt << 1. At λ·dt > 0.1 the probability of ≥2 jumps per
+            // step becomes non-negligible; at λ·dt ≥ 1 the trial degenerates
+            // to a deterministic jump every step. Fail fast.
+            if (jumpLambdaDt > 0.1) {
+                throw JsonRpcFaultException.validationError(
+                    "jumpIntensity too high for time step: lambda*dt=" +
+                    String.format("%.4f", jumpLambdaDt) + " > 0.1. " +
+                    "Reduce jumpIntensity or increase nPeriodsPerYear.");
+            }
+        }
+        double drift = (mu - 0.5 * sigma * sigma - jumpCompensation) * dt;
         double volSqrtDt = sigma * Math.sqrt(dt);
 
         // ── PRNG: seeded for reproducibility, unseeded for production 
─────────
@@ -386,7 +414,6 @@ public class FinancialBenchmarkService {
 
         double[] finalValues = new double[nSims];
         double sumFinal = 0.0;
-        double sumSqFinal = 0.0;
         int profitCount = 0;
         double maxDrawdown = 0.0;
 
@@ -399,7 +426,18 @@ public class FinancialBenchmarkService {
 
             for (int period = 0; period < nPeriods; period++) {
                 double z = rng.nextGaussian();
-                value *= Math.exp(drift + volSqrtDt * z);
+                double exponent = drift + volSqrtDt * z;
+
+                // Merton jump component: compound Poisson process.
+                // At each step, a jump occurs with probability λ·dt.
+                // The jump magnitude is log-normal: J = exp(μ_J + σ_J · W)
+                // where W ~ N(0,1) is independent of the diffusion Z.
+                if (isMerton && rng.nextDouble() < jumpLambdaDt) {
+                    double w = rng.nextGaussian();
+                    exponent += jumpMean + jumpVol * w;
+                }
+
+                value *= Math.exp(exponent);
 
                 if (value > peak) {
                     peak = value;
@@ -411,7 +449,6 @@ public class FinancialBenchmarkService {
 
             finalValues[sim] = value;
             sumFinal += value;
-            sumSqFinal += value * value;
             if (value > initialValue) profitCount++;
             if (simMaxDrawdown > maxDrawdown) maxDrawdown = simMaxDrawdown;
         }
@@ -419,18 +456,28 @@ public class FinancialBenchmarkService {
         long elapsedUs = (System.nanoTime() - startNs) / 1_000;
 
         // ── Statistics 
────────────────────────────────────────────────────────
+        // Two-pass algorithm for variance: the one-pass formula
+        // (sumSq/N − mean²) suffers catastrophic cancellation when
+        // stdDev << mean (common for low-vol strategies). Two-pass:
+        // compute mean first, then sum squared deviations.
         double mean = sumFinal / nSims;
-        double variance = (sumSqFinal / nSims) - (mean * mean);
-        if (variance < 0.0) variance = 0.0;
+        double sumSqDiff = 0.0;
+        for (int i = 0; i < nSims; i++) {
+            double d = finalValues[i] - mean;
+            sumSqDiff += d * d;
+        }
+        double variance = sumSqDiff / nSims;
 
         // Sort ascending so finalValues[0] is the worst outcome and
         // finalValues[nSims-1] is the best.  VaR and CVaR then become
-        // simple index lookups: the p-th percentile loss corresponds to
-        // finalValues[floor(p * nSims)].
+        // simple index lookups.
         Arrays.sort(finalValues);
 
-        int idx5  = (int)(0.05 * nSims);
-        int idx1  = (int)(0.01 * nSims);
+        // Percentile indexing: ceil(p·N) − 1 selects the k-th order
+        // statistic such that exactly floor(p·N) observations are strictly
+        // below the VaR level. Matches the standard quantile definition.
+        int idx5  = Math.max(0, (int) Math.ceil(0.05 * nSims) - 1);
+        int idx1  = Math.max(0, (int) Math.ceil(0.01 * nSims) - 1);
 
         // Sample median of a sorted array: for odd N take the middle element,
         // for even N average the two central elements.  Using a single index
@@ -467,7 +514,7 @@ public class FinancialBenchmarkService {
             for (int pi = 0; pi < maxPct; pi++) {
                 double p = request.getPercentiles()[pi];
                 if (p <= 0.0 || p >= 1.0) continue;
-                int idx = (int)(p * nSims);
+                int idx = Math.max(0, (int) Math.ceil(p * nSims) - 1);
                 if (idx >= nSims) idx = nSims - 1;
                 percentileVars.add(new MonteCarloResponse.PercentileVar(
                     p, initialValue - finalValues[idx]));
@@ -475,6 +522,7 @@ public class FinancialBenchmarkService {
         }
 
         MonteCarloResponse response = new MonteCarloResponse();
+        response.setModel(isMerton ? "merton" : "gbm");
         response.setStatus("SUCCESS");
         response.setMeanFinalValue(mean);
         response.setMedianFinalValue(median);
diff --git 
a/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/MonteCarloRequest.java
 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/MonteCarloRequest.java
index 2e2d668742..c9d07361ad 100644
--- 
a/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/MonteCarloRequest.java
+++ 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/MonteCarloRequest.java
@@ -21,10 +21,23 @@ package userguide.springboot.webservices;
 /**
  * Request for Monte Carlo Value-at-Risk simulation.
  *
- * <p>Simulates portfolio value paths using Geometric Brownian Motion:
+ * <p>Simulates portfolio value paths using one of two stochastic models:
+ *
+ * <p><b>GBM</b> (model="gbm", default): Geometric Brownian Motion.
  * <pre>S(t+dt) = S(t) × exp((μ − σ²/2)·dt + σ·√dt·Z)</pre>
  * where dt = 1/nPeriodsPerYear and Z ~ N(0,1).
  *
+ * <p><b>Merton jump-diffusion</b> (model="merton"): GBM plus a compound
+ * Poisson jump process that captures fat tails and discontinuities
+ * (crashes, gaps) that GBM systematically understates.
+ * <pre>S(t+dt) = S(t) × exp((μ − σ²/2 − λk)·dt + σ·√dt·Z) × J</pre>
+ * where J = exp(μ_J + σ_J·W) with probability λ·dt per step, else J = 1.
+ * Z, W ~ N(0,1) independent. k = exp(μ_J + σ_J²/2) − 1.
+ * The −λk drift correction preserves E[S(T)] = S(0)·exp(μ·T).
+ *
+ * <p>Reference: Merton, R. (1976). "Option pricing when underlying stock
+ * returns are discontinuous." J. Financial Economics, 3(1-2): 125-144.
+ *
  * <p>All fields have defaults so a minimal {@code {}} request body is valid.
  *
  * <h3>Example</h3>
@@ -196,6 +209,38 @@ public class MonteCarloRequest {
      */
     private double[] percentiles = {0.01, 0.05};
 
+    /**
+     * Simulation model: "gbm" (default) or "merton".
+     *
+     * <p>GBM is the textbook baseline — constant vol, continuous paths.
+     * Merton adds a compound Poisson jump process for fat tails.
+     * See class javadoc for the full model equations.
+     */
+    private String model = "gbm";
+
+    /**
+     * Jump intensity (Merton only): expected number of jumps per YEAR
+     * (Poisson λ). Must be &gt;= 0. Default: 1.0. At λ=1, approximately
+     * one jump per simulated year. Higher values increase kurtosis.
+     * Ignored when model="gbm".
+     */
+    private double jumpIntensity = 1.0;
+
+    /**
+     * Mean of the log-normal jump size distribution (Merton only).
+     * Negative values produce downward jumps on average (crashes).
+     * Default: -0.03 (average jump ≈ 3% drop). Ignored when model="gbm".
+     */
+    private double jumpMean = -0.03;
+
+    /**
+     * Volatility of the log-normal jump size (Merton only). Controls how
+     * variable individual jump magnitudes are. Must be &gt;= 0.
+     * Default: 0.05. At jumpVol=0 every jump has exactly size
+     * exp(jumpMean). Ignored when model="gbm".
+     */
+    private double jumpVol = 0.05;
+
     /** Optional identifier echoed in the response for request tracing. */
     private String requestId;
 
@@ -218,6 +263,10 @@ public class MonteCarloRequest {
     public int getNPeriodsPerYear() { return nPeriodsPerYear > 0 ? 
nPeriodsPerYear : 252; }
     public double[] getPercentiles() { return percentiles != null ? 
percentiles : new double[]{0.01, 0.05}; }
     public String getRequestId() { return requestId; }
+    public String getModel() { return model != null ? model : "gbm"; }
+    public double getJumpIntensity() { return jumpIntensity >= 0 ? 
jumpIntensity : 1.0; }
+    public double getJumpMean() { return jumpMean; }
+    public double getJumpVol() { return jumpVol >= 0 ? jumpVol : 0.05; }
 
     // ── setters 
──────────────────────────────────────────────────────────────
 
@@ -230,4 +279,8 @@ public class MonteCarloRequest {
     public void setNPeriodsPerYear(int nPeriodsPerYear) { this.nPeriodsPerYear 
= nPeriodsPerYear; }
     public void setPercentiles(double[] percentiles) { this.percentiles = 
percentiles; }
     public void setRequestId(String requestId) { this.requestId = requestId; }
+    public void setModel(String model) { this.model = model; }
+    public void setJumpIntensity(double jumpIntensity) { this.jumpIntensity = 
jumpIntensity; }
+    public void setJumpMean(double jumpMean) { this.jumpMean = jumpMean; }
+    public void setJumpVol(double jumpVol) { this.jumpVol = jumpVol; }
 }
diff --git 
a/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/MonteCarloResponse.java
 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/MonteCarloResponse.java
index 6750f807b9..ac31d79751 100644
--- 
a/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/MonteCarloResponse.java
+++ 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/MonteCarloResponse.java
@@ -81,6 +81,9 @@ public class MonteCarloResponse {
     /** JVM heap used at response time in MB */
     private long memoryUsedMb;
 
+    /** Model used for this simulation: "gbm" or "merton" */
+    private String model;
+
     /** Echoed from request */
     private String requestId;
 
@@ -161,4 +164,7 @@ public class MonteCarloResponse {
 
     public String getRequestId() { return requestId; }
     public void setRequestId(String requestId) { this.requestId = requestId; }
+
+    public String getModel() { return model; }
+    public void setModel(String model) { this.model = model; }
 }

Reply via email to