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 82e6e243a6a4d378a2e22094cbf8780d3ca270c2
Author: Robert Lazarski <[email protected]>
AuthorDate: Mon Apr 6 13:54:28 2026 -1000

    springbootdemo-tomcat11: apply Gemini review fixes + add 
FinancialBenchmarkServiceTest
    
    Gemini 2.5 Pro LOW findings applied to the Java financial benchmark service:
    
    - FinancialBenchmarkService: add logger.warn() to all validation failure 
paths
      so rejected requests appear in server logs (previously silent)
    - FinancialBenchmarkService: runtimeInfo() now reports heap tier 
classification
      ("JVM heap tier: 16+ GB") instead of Java version string (information 
disclosure)
    - FinancialBenchmarkService: simplify monteCarlo() — remove redundant 
ternary
      defaults now that MonteCarloRequest getters enforce them
    - MonteCarloRequest: getters enforce defaults (nSimulations, nPeriods,
      initialValue, nPeriodsPerYear) so service code has no ternary clutter
    
    CRITICAL finding applied: add unit test coverage 
(FinancialBenchmarkServiceTest):
    - portfolioVariance: 2-asset known result (σ²=0.0355), single asset, flat 
matrix,
      monthly nPeriodsPerYear, normalizeWeights rescaling, 6 validation paths
    - monteCarlo: seeded reproducibility, defaults, zero volatility, custom 
percentiles,
      monthly vs daily nPeriodsPerYear, max-simulations cap, 
negative-volatility rejection
    - scenarioAnalysis: single-asset known result (E[r]=0.04, upside=120, 
downside=80,
      U/D=1.5), 200-asset hash vs linear speedup, useHashLookup=false, 4 
validation paths
    - MonteCarloRequest getter defaults verified independently
    - 30 tests total; all paths exercised without Spring context (plain unit 
tests)
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
 .../webservices/FinancialBenchmarkService.java     |  74 +--
 .../springboot/webservices/MonteCarloRequest.java  |  10 +-
 .../webservices/FinancialBenchmarkServiceTest.java | 528 +++++++++++++++++++++
 3 files changed, 577 insertions(+), 35 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 8482b3b47d..b1c6622022 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
@@ -94,26 +94,30 @@ public class FinancialBenchmarkService {
 
         int n = request.getNAssets();
         if (n <= 0 || n > MAX_ASSETS) {
-            return PortfolioVarianceResponse.failed(
-                "n_assets=" + n + " is out of range [1, " + MAX_ASSETS + "].");
+            String err = "n_assets=" + n + " is out of range [1, " + 
MAX_ASSETS + "].";
+            logger.warn(logPrefix + "validation failed: " + err);
+            return PortfolioVarianceResponse.failed(err);
         }
 
         // ── Resolve covariance matrix 
─────────────────────────────────────────
         double[][] cov = resolveCovarianceMatrix(request, n);
         if (cov == null) {
-            return PortfolioVarianceResponse.failed(
-                "Missing or malformed \"covarianceMatrix\": provide a " + n + 
"×" + n +
-                " 2D array or a flat array of " + (long) n * n + " elements in 
row-major order.");
+            String err = "Missing or malformed \"covarianceMatrix\": provide a 
" + n + "×" + n +
+                " 2D array or a flat array of " + (long) n * n + " elements in 
row-major order.";
+            logger.warn(logPrefix + "validation failed: " + err);
+            return PortfolioVarianceResponse.failed(err);
         }
         if (cov.length != n) {
-            return PortfolioVarianceResponse.failed(
-                "covarianceMatrix row count " + cov.length + " != nAssets " + 
n + ".");
+            String err = "covarianceMatrix row count " + cov.length + " != 
nAssets " + n + ".";
+            logger.warn(logPrefix + "validation failed: " + err);
+            return PortfolioVarianceResponse.failed(err);
         }
         for (int i = 0; i < n; i++) {
             if (cov[i] == null || cov[i].length != n) {
-                return PortfolioVarianceResponse.failed(
-                    "covarianceMatrix row " + i + " has " +
-                    (cov[i] == null ? 0 : cov[i].length) + " columns, expected 
" + n + ".");
+                String err = "covarianceMatrix row " + i + " has " +
+                    (cov[i] == null ? 0 : cov[i].length) + " columns, expected 
" + n + ".";
+                logger.warn(logPrefix + "validation failed: " + err);
+                return PortfolioVarianceResponse.failed(err);
             }
         }
 
@@ -125,9 +129,10 @@ public class FinancialBenchmarkService {
         boolean weightsNormalized = false;
         if (request.isNormalizeWeights()) {
             if (weightSum <= 0.0) {
-                return PortfolioVarianceResponse.failed(
-                    "normalizeWeights=true but weights sum to " + weightSum +
-                    ". Cannot normalize a zero-weight portfolio.");
+                String err = "normalizeWeights=true but weights sum to " + 
weightSum +
+                    ". Cannot normalize a zero-weight portfolio.";
+                logger.warn(logPrefix + "validation failed: " + err);
+                return PortfolioVarianceResponse.failed(err);
             }
             if (Math.abs(weightSum - 1.0) > 1e-10) {
                 for (int i = 0; i < n; i++) weights[i] /= weightSum;
@@ -136,10 +141,11 @@ public class FinancialBenchmarkService {
             }
         } else {
             if (Math.abs(weightSum - 1.0) > 1e-4) {
-                return PortfolioVarianceResponse.failed(
-                    "weights sum to " + String.format("%.8f", weightSum) +
+                String err = "weights sum to " + String.format("%.8f", 
weightSum) +
                     ", expected 1.0 (tolerance 1e-4). " +
-                    "Pass normalizeWeights=true to rescale automatically.");
+                    "Pass normalizeWeights=true to rescale automatically.";
+                logger.warn(logPrefix + "validation failed: " + err);
+                return PortfolioVarianceResponse.failed(err);
             }
         }
 
@@ -201,11 +207,9 @@ public class FinancialBenchmarkService {
             return MonteCarloResponse.failed("Request must not be null.");
         }
 
-        int nSims = Math.min(
-            request.getNSimulations() > 0 ? request.getNSimulations() : 10_000,
-            MAX_SIMULATIONS);
-        int nPeriods = request.getNPeriods() > 0 ? request.getNPeriods() : 252;
-        double initialValue = request.getInitialValue() > 0 ? 
request.getInitialValue() : 1_000_000.0;
+        int nSims = Math.min(request.getNSimulations(), MAX_SIMULATIONS);
+        int nPeriods = request.getNPeriods();
+        double initialValue = request.getInitialValue();
         double mu = request.getExpectedReturn();
         double sigma = request.getVolatility();
         int npy = request.getNPeriodsPerYear();
@@ -341,8 +345,9 @@ public class FinancialBenchmarkService {
         List<ScenarioAnalysisRequest.AssetScenario> assets = 
request.getAssets();
         int nAssets = assets.size();
         if (nAssets > MAX_ASSETS) {
-            return ScenarioAnalysisResponse.failed(
-                "assets count " + nAssets + " exceeds maximum " + MAX_ASSETS + 
".");
+            String err = "assets count " + nAssets + " exceeds maximum " + 
MAX_ASSETS + ".";
+            logger.warn(logPrefix + "validation failed: " + err);
+            return ScenarioAnalysisResponse.failed(err);
         }
 
         double probTolerance = request.getProbTolerance();
@@ -357,13 +362,15 @@ public class FinancialBenchmarkService {
                 probSum += s.getProbability();
             }
             if (Math.abs(probSum - 1.0) > probTolerance) {
-                return ScenarioAnalysisResponse.failed(String.format(
+                String err = String.format(
                     "Asset index %d (id=%d): scenario probabilities sum to 
%.8f, " +
                     "expected 1.0 (tolerance %.2g). " +
                     "All %d scenario probabilities must sum to exactly 1.0. " +
                     "Pass probTolerance to adjust validation strictness.",
                     i, asset.getAssetId(), probSum, probTolerance,
-                    asset.getScenarios().size()));
+                    asset.getScenarios().size());
+                logger.warn(logPrefix + "validation failed: " + err);
+                return ScenarioAnalysisResponse.failed(err);
             }
         }
 
@@ -515,12 +522,19 @@ public class FinancialBenchmarkService {
         return (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024);
     }
 
-    /** Human-readable JVM / runtime identifier for response metadata. */
+    /**
+     * Runtime identifier for response metadata.
+     * Reports the JVM family and heap class without exposing version numbers
+     * or precise memory configuration (which would aid fingerprinting).
+     */
     private String runtimeInfo() {
         Runtime rt = Runtime.getRuntime();
-        return String.format("Java %s (heap: %d MB max / %d MB total)",
-            System.getProperty("java.version"),
-            rt.maxMemory() / (1024 * 1024),
-            rt.totalMemory() / (1024 * 1024));
+        long maxMb = rt.maxMemory() / (1024 * 1024);
+        // Heap tier, not exact size — enough for C vs JVM comparison context
+        String heapTier = maxMb >= 16_000 ? "16+ GB" :
+                          maxMb >=  8_000 ? "8+ GB"  :
+                          maxMb >=  4_000 ? "4+ GB"  :
+                          maxMb >=  2_000 ? "2+ GB"  : "< 2 GB";
+        return "Java (JVM heap tier: " + heapTier + ")";
     }
 }
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 9ea6ed8d5a..78fe5a69b1 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
@@ -80,16 +80,16 @@ public class MonteCarloRequest {
     /** Optional identifier echoed in the response for request tracing. */
     private String requestId;
 
-    // ── getters 
──────────────────────────────────────────────────────────────
+    // ── getters — all enforce defaults so service code has no ternary 
clutter ─
 
-    public int getNSimulations() { return nSimulations; }
-    public int getNPeriods() { return nPeriods; }
-    public double getInitialValue() { return initialValue; }
+    public int getNSimulations() { return nSimulations > 0 ? nSimulations : 
10_000; }
+    public int getNPeriods() { return nPeriods > 0 ? nPeriods : 252; }
+    public double getInitialValue() { return initialValue > 0 ? initialValue : 
1_000_000.0; }
     public double getExpectedReturn() { return expectedReturn; }
     public double getVolatility() { return volatility; }
     public long getRandomSeed() { return randomSeed; }
     public int getNPeriodsPerYear() { return nPeriodsPerYear > 0 ? 
nPeriodsPerYear : 252; }
-    public double[] getPercentiles() { return percentiles; }
+    public double[] getPercentiles() { return percentiles != null ? 
percentiles : new double[]{0.01, 0.05}; }
     public String getRequestId() { return requestId; }
 
     // ── setters 
──────────────────────────────────────────────────────────────
diff --git 
a/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/test/java/userguide/springboot/webservices/FinancialBenchmarkServiceTest.java
 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/test/java/userguide/springboot/webservices/FinancialBenchmarkServiceTest.java
new file mode 100644
index 0000000000..2cbfc4dbaa
--- /dev/null
+++ 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/test/java/userguide/springboot/webservices/FinancialBenchmarkServiceTest.java
@@ -0,0 +1,528 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package userguide.springboot.webservices;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for {@link FinancialBenchmarkService}.
+ *
+ * <p>Covers:
+ * <ul>
+ *   <li>Happy-path results for all three operations with known inputs</li>
+ *   <li>Input validation: null, out-of-range, malformed arrays, probability 
sums</li>
+ *   <li>Edge cases: single asset, zero volatility, weight normalization, 
seeded reproducibility</li>
+ *   <li>New API fields: nPeriodsPerYear, percentiles, normalizeWeights, 
probTolerance</li>
+ * </ul>
+ */
+class FinancialBenchmarkServiceTest {
+
+    private FinancialBenchmarkService service;
+
+    @BeforeEach
+    void setUp() {
+        service = new FinancialBenchmarkService();
+    }
+
+    // ═══════════════════════════════════════════════════════════════════════
+    // portfolioVariance — happy path
+    // ═══════════════════════════════════════════════════════════════════════
+
+    @Test
+    void testPortfolioVariance_twoAssets_knownResult() {
+        // 50/50 portfolio, cov = [[0.04, 0.006],[0.006, 0.09]]
+        // σ²_p = 0.5²×0.04 + 2×0.5×0.5×0.006 + 0.5²×0.09 = 0.01 + 0.003 + 
0.0225 = 0.0355
+        PortfolioVarianceRequest req = new PortfolioVarianceRequest();
+        req.setWeights(new double[]{0.5, 0.5});
+        req.setCovarianceMatrix(new double[][]{{0.04, 0.006}, {0.006, 0.09}});
+
+        PortfolioVarianceResponse resp = service.portfolioVariance(req);
+
+        assertEquals("SUCCESS", resp.getStatus(), "status should be SUCCESS");
+        assertEquals(0.0355, resp.getPortfolioVariance(), 1e-10, "variance 
mismatch");
+        assertEquals(Math.sqrt(0.0355), resp.getPortfolioVolatility(), 1e-10, 
"volatility mismatch");
+        assertEquals(Math.sqrt(0.0355) * Math.sqrt(252), 
resp.getAnnualizedVolatility(), 1e-9,
+            "annualized volatility uses nPeriodsPerYear=252 by default");
+        assertEquals(1.0, resp.getWeightSum(), 1e-10, "weight_sum should be 
1.0");
+        assertFalse(resp.isWeightsNormalized(), "weights should not be 
normalized when already summing to 1.0");
+        assertTrue(resp.getMatrixOperations() == 4, "2x2 matrix = 4 
operations");
+    }
+
+    @Test
+    void testPortfolioVariance_singleAsset() {
+        // Diagonal 1×1: σ²_p = w² × σ² = 1² × 0.04 = 0.04
+        PortfolioVarianceRequest req = new PortfolioVarianceRequest();
+        req.setWeights(new double[]{1.0});
+        req.setCovarianceMatrix(new double[][]{{0.04}});
+
+        PortfolioVarianceResponse resp = service.portfolioVariance(req);
+
+        assertEquals("SUCCESS", resp.getStatus());
+        assertEquals(0.04, resp.getPortfolioVariance(), 1e-12);
+        assertEquals(0.2, resp.getPortfolioVolatility(), 1e-12);
+    }
+
+    @Test
+    void testPortfolioVariance_flatMatrixFormat() {
+        // Same 2-asset test, but using flat array
+        PortfolioVarianceRequest req = new PortfolioVarianceRequest();
+        req.setWeights(new double[]{0.5, 0.5});
+        req.setCovarianceMatrixFlat(new double[]{0.04, 0.006, 0.006, 0.09});
+
+        PortfolioVarianceResponse resp = service.portfolioVariance(req);
+
+        assertEquals("SUCCESS", resp.getStatus());
+        assertEquals(0.0355, resp.getPortfolioVariance(), 1e-10);
+    }
+
+    @Test
+    void testPortfolioVariance_nPeriodsPerYear() {
+        // Monthly data: nPeriodsPerYear=12
+        PortfolioVarianceRequest req = new PortfolioVarianceRequest();
+        req.setWeights(new double[]{1.0});
+        req.setCovarianceMatrix(new double[][]{{0.04}});
+        req.setNPeriodsPerYear(12);
+
+        PortfolioVarianceResponse resp = service.portfolioVariance(req);
+
+        assertEquals("SUCCESS", resp.getStatus());
+        assertEquals(Math.sqrt(0.04) * Math.sqrt(12), 
resp.getAnnualizedVolatility(), 1e-10,
+            "annualized volatility should use nPeriodsPerYear=12");
+    }
+
+    @Test
+    void testPortfolioVariance_normalizeWeights_rescalesAndReports() {
+        // Weights sum to 2.0 — should be normalized to {0.5, 0.5}
+        PortfolioVarianceRequest req = new PortfolioVarianceRequest();
+        req.setWeights(new double[]{1.0, 1.0});
+        req.setCovarianceMatrix(new double[][]{{0.04, 0.006}, {0.006, 0.09}});
+        req.setNormalizeWeights(true);
+
+        PortfolioVarianceResponse resp = service.portfolioVariance(req);
+
+        assertEquals("SUCCESS", resp.getStatus());
+        assertEquals(2.0, resp.getWeightSum(), 1e-10, "raw weight sum before 
normalization");
+        assertTrue(resp.isWeightsNormalized(), "should report normalization 
was applied");
+        assertEquals(0.0355, resp.getPortfolioVariance(), 1e-10,
+            "after normalization result equals 50/50 portfolio variance");
+    }
+
+    @Test
+    void testPortfolioVariance_normalizeWeights_alreadyUnit_noRescale() {
+        PortfolioVarianceRequest req = new PortfolioVarianceRequest();
+        req.setWeights(new double[]{0.5, 0.5});
+        req.setCovarianceMatrix(new double[][]{{0.04, 0.006}, {0.006, 0.09}});
+        req.setNormalizeWeights(true);
+
+        PortfolioVarianceResponse resp = service.portfolioVariance(req);
+
+        assertEquals("SUCCESS", resp.getStatus());
+        assertFalse(resp.isWeightsNormalized(), "already-unit weights should 
not be marked normalized");
+    }
+
+    // ═══════════════════════════════════════════════════════════════════════
+    // portfolioVariance — validation
+    // ═══════════════════════════════════════════════════════════════════════
+
+    @Test
+    void testPortfolioVariance_nullRequest() {
+        PortfolioVarianceResponse resp = service.portfolioVariance(null);
+        assertEquals("FAILED", resp.getStatus());
+        assertNotNull(resp.getErrorMessage());
+    }
+
+    @Test
+    void testPortfolioVariance_nAssetsExceedsMax() {
+        PortfolioVarianceRequest req = new PortfolioVarianceRequest();
+        req.setWeights(new double[FinancialBenchmarkService.MAX_ASSETS + 1]);
+        // No matrix set — should fail on nAssets check first
+        PortfolioVarianceResponse resp = service.portfolioVariance(req);
+        assertEquals("FAILED", resp.getStatus());
+        assertTrue(resp.getErrorMessage().contains("out of range"),
+            "error must mention out-of-range: " + resp.getErrorMessage());
+    }
+
+    @Test
+    void testPortfolioVariance_missingCovarianceMatrix() {
+        PortfolioVarianceRequest req = new PortfolioVarianceRequest();
+        req.setWeights(new double[]{0.5, 0.5});
+        // No matrix set
+
+        PortfolioVarianceResponse resp = service.portfolioVariance(req);
+        assertEquals("FAILED", resp.getStatus());
+        assertTrue(resp.getErrorMessage().contains("covarianceMatrix"),
+            "error must mention covarianceMatrix: " + resp.getErrorMessage());
+    }
+
+    @Test
+    void testPortfolioVariance_weightsDontSumToOne() {
+        PortfolioVarianceRequest req = new PortfolioVarianceRequest();
+        req.setWeights(new double[]{0.3, 0.3}); // sum=0.6
+        req.setCovarianceMatrix(new double[][]{{0.04, 0.006}, {0.006, 0.09}});
+
+        PortfolioVarianceResponse resp = service.portfolioVariance(req);
+        assertEquals("FAILED", resp.getStatus());
+        assertTrue(resp.getErrorMessage().contains("normalizeWeights"),
+            "error should suggest normalizeWeights: " + 
resp.getErrorMessage());
+    }
+
+    @Test
+    void testPortfolioVariance_normalizeWeights_zeroWeights_fails() {
+        PortfolioVarianceRequest req = new PortfolioVarianceRequest();
+        req.setWeights(new double[]{0.0, 0.0});
+        req.setCovarianceMatrix(new double[][]{{0.04, 0.006}, {0.006, 0.09}});
+        req.setNormalizeWeights(true);
+
+        PortfolioVarianceResponse resp = service.portfolioVariance(req);
+        assertEquals("FAILED", resp.getStatus());
+        assertTrue(resp.getErrorMessage().contains("zero-weight"),
+            "error must mention zero-weight: " + resp.getErrorMessage());
+    }
+
+    @Test
+    void testPortfolioVariance_flatMatrix_wrongLength_fails() {
+        PortfolioVarianceRequest req = new PortfolioVarianceRequest();
+        req.setWeights(new double[]{0.5, 0.5});
+        req.setCovarianceMatrixFlat(new double[]{0.04, 0.006, 0.006}); // 
length 3, expected 4
+
+        PortfolioVarianceResponse resp = service.portfolioVariance(req);
+        assertEquals("FAILED", resp.getStatus());
+    }
+
+    // ═══════════════════════════════════════════════════════════════════════
+    // monteCarlo — happy path
+    // ═══════════════════════════════════════════════════════════════════════
+
+    @Test
+    void testMonteCarlo_seededRunIsReproducible() {
+        MonteCarloRequest req1 = new MonteCarloRequest();
+        req1.setNSimulations(1000);
+        req1.setRandomSeed(42L);
+
+        MonteCarloRequest req2 = new MonteCarloRequest();
+        req2.setNSimulations(1000);
+        req2.setRandomSeed(42L);
+
+        MonteCarloResponse r1 = service.monteCarlo(req1);
+        MonteCarloResponse r2 = service.monteCarlo(req2);
+
+        assertEquals("SUCCESS", r1.getStatus());
+        assertEquals(r1.getVar95(), r2.getVar95(), 1e-9, "seeded runs must be 
identical");
+        assertEquals(r1.getMeanFinalValue(), r2.getMeanFinalValue(), 1e-9);
+    }
+
+    @Test
+    void testMonteCarlo_defaults_succeed() {
+        MonteCarloResponse resp = service.monteCarlo(new MonteCarloRequest());
+        assertEquals("SUCCESS", resp.getStatus());
+        assertTrue(resp.getMeanFinalValue() > 0, "mean final value should be 
positive");
+        assertTrue(resp.getVar95() >= 0, "VaR95 should be non-negative");
+    }
+
+    @Test
+    void testMonteCarlo_zeroVolatility_allPathsEqual() {
+        MonteCarloRequest req = new MonteCarloRequest();
+        req.setNSimulations(100);
+        req.setVolatility(0.0);
+        req.setExpectedReturn(0.0);
+        req.setRandomSeed(1L);
+
+        MonteCarloResponse resp = service.monteCarlo(req);
+
+        assertEquals("SUCCESS", resp.getStatus());
+        // With zero drift and zero volatility, all paths end at initialValue
+        assertEquals(resp.getInitialValue(), resp.getMeanFinalValue(), 1.0,
+            "zero vol, zero drift: mean should equal initialValue");
+    }
+
+    @Test
+    void testMonteCarlo_customPercentiles() {
+        MonteCarloRequest req = new MonteCarloRequest();
+        req.setNSimulations(1000);
+        req.setRandomSeed(7L);
+        req.setPercentiles(new double[]{0.01, 0.05, 0.10});
+
+        MonteCarloResponse resp = service.monteCarlo(req);
+
+        assertEquals("SUCCESS", resp.getStatus());
+        assertNotNull(resp.getPercentileVars(), "percentileVars should not be 
null");
+        assertEquals(3, resp.getPercentileVars().size(), "should have 3 
percentile entries");
+        assertEquals(0.01, resp.getPercentileVars().get(0).getPercentile(), 
1e-9);
+        assertEquals(0.05, resp.getPercentileVars().get(1).getPercentile(), 
1e-9);
+        assertEquals(0.10, resp.getPercentileVars().get(2).getPercentile(), 
1e-9);
+        // VaR values should be non-decreasing as percentile decreases (deeper 
tail = bigger loss)
+        assertTrue(resp.getPercentileVars().get(0).getVar() >= 
resp.getPercentileVars().get(1).getVar(),
+            "VaR at 1% >= VaR at 5% (deeper tail)");
+    }
+
+    @Test
+    void testMonteCarlo_nPeriodsPerYear_affectsDt() {
+        // Same parameters, different nPeriodsPerYear: monthly (12) vs daily 
(252)
+        // Monthly has much larger per-step vol (σ×√(1/12) vs σ×√(1/252))
+        // so final value distribution should be wider
+        MonteCarloRequest daily = new MonteCarloRequest();
+        daily.setNSimulations(5000);
+        daily.setNPeriods(12);
+        daily.setNPeriodsPerYear(252);
+        daily.setRandomSeed(99L);
+
+        MonteCarloRequest monthly = new MonteCarloRequest();
+        monthly.setNSimulations(5000);
+        monthly.setNPeriods(12);
+        monthly.setNPeriodsPerYear(12);
+        monthly.setRandomSeed(99L);
+
+        MonteCarloResponse rDaily = service.monteCarlo(daily);
+        MonteCarloResponse rMonthly = service.monteCarlo(monthly);
+
+        assertEquals("SUCCESS", rDaily.getStatus());
+        assertEquals("SUCCESS", rMonthly.getStatus());
+        // Monthly steps have larger vol per step → higher StdDev of final 
values
+        assertTrue(rMonthly.getStdDevFinalValue() > 
rDaily.getStdDevFinalValue(),
+            "Monthly steps should produce wider distribution than daily steps 
for same nPeriods");
+    }
+
+    @Test
+    void testMonteCarlo_capsAtMaxSimulations() {
+        MonteCarloRequest req = new MonteCarloRequest();
+        req.setNSimulations(Integer.MAX_VALUE); // way over limit
+
+        MonteCarloResponse resp = service.monteCarlo(req);
+        // Should not OOM — service caps at MAX_SIMULATIONS
+        assertEquals("SUCCESS", resp.getStatus());
+    }
+
+    // ═══════════════════════════════════════════════════════════════════════
+    // monteCarlo — validation
+    // ═══════════════════════════════════════════════════════════════════════
+
+    @Test
+    void testMonteCarlo_negativeVolatility_fails() {
+        MonteCarloRequest req = new MonteCarloRequest();
+        req.setVolatility(-0.1);
+
+        MonteCarloResponse resp = service.monteCarlo(req);
+        assertEquals("FAILED", resp.getStatus());
+        assertTrue(resp.getErrorMessage().contains("volatility"),
+            "error must mention volatility: " + resp.getErrorMessage());
+    }
+
+    // ═══════════════════════════════════════════════════════════════════════
+    // scenarioAnalysis — happy path
+    // ═══════════════════════════════════════════════════════════════════════
+
+    @Test
+    void testScenarioAnalysis_singleAsset_knownResult() {
+        // Asset: price=100, position=10 shares
+        // Scenario 1: price=120, prob=0.6  → gain = 20×10 = 200
+        // Scenario 2: price=80,  prob=0.4  → loss = 20×10 = 200
+        // E[r] = 0.6×(120/100-1) + 0.4×(80/100-1) = 0.6×0.2 + 0.4×(-0.2) = 
0.12 - 0.08 = 0.04
+        // Upside  = 0.6 × 20 × 10 = 120
+        // Downside= 0.4 × 20 × 10 = 80
+
+        ScenarioAnalysisRequest.AssetScenario asset = new 
ScenarioAnalysisRequest.AssetScenario();
+        asset.setAssetId(1001L);
+        asset.setCurrentPrice(100.0);
+        asset.setPositionSize(10.0);
+
+        ScenarioAnalysisRequest.Scenario s1 = new 
ScenarioAnalysisRequest.Scenario();
+        s1.setPrice(120.0); s1.setProbability(0.6);
+        ScenarioAnalysisRequest.Scenario s2 = new 
ScenarioAnalysisRequest.Scenario();
+        s2.setPrice(80.0); s2.setProbability(0.4);
+        asset.setScenarios(Arrays.asList(s1, s2));
+
+        ScenarioAnalysisRequest req = new ScenarioAnalysisRequest();
+        req.setAssets(List.of(asset));
+        req.setRequestId("test-001");
+
+        ScenarioAnalysisResponse resp = service.scenarioAnalysis(req);
+
+        assertEquals("SUCCESS", resp.getStatus());
+        assertEquals(0.04, resp.getExpectedReturn(), 1e-10, "expected return");
+        assertEquals(120.0, resp.getUpsidePotential(), 1e-10, "upside 
potential");
+        assertEquals(80.0, resp.getDownsideRisk(), 1e-10, "downside risk");
+        assertEquals(1.5, resp.getUpsideDownsideRatio(), 1e-10, "U/D ratio = 
120/80 = 1.5");
+        assertEquals("test-001", resp.getRequestId(), "requestId should be 
echoed");
+    }
+
+    @Test
+    void testScenarioAnalysis_hashLookup_fasterThanLinear_largePortfolio() {
+        // Build 200-asset portfolio to make timing difference detectable
+        List<ScenarioAnalysisRequest.AssetScenario> assets = new 
java.util.ArrayList<>();
+        for (int i = 0; i < 200; i++) {
+            ScenarioAnalysisRequest.AssetScenario asset = new 
ScenarioAnalysisRequest.AssetScenario();
+            asset.setAssetId(1000L + i);
+            asset.setCurrentPrice(100.0 + i);
+            asset.setPositionSize(10.0);
+
+            ScenarioAnalysisRequest.Scenario s1 = new 
ScenarioAnalysisRequest.Scenario();
+            s1.setPrice(110.0 + i); s1.setProbability(0.5);
+            ScenarioAnalysisRequest.Scenario s2 = new 
ScenarioAnalysisRequest.Scenario();
+            s2.setPrice(90.0 + i); s2.setProbability(0.5);
+            asset.setScenarios(Arrays.asList(s1, s2));
+            assets.add(asset);
+        }
+
+        ScenarioAnalysisRequest req = new ScenarioAnalysisRequest();
+        req.setAssets(assets);
+        req.setUseHashLookup(true);
+
+        ScenarioAnalysisResponse resp = service.scenarioAnalysis(req);
+
+        assertEquals("SUCCESS", resp.getStatus());
+        assertNotNull(resp.getLookupBenchmark(), "benchmark string should be 
present");
+        assertTrue(resp.getLinearLookupUs() > 0 || resp.getHashLookupUs() >= 0,
+            "timing fields should be set");
+    }
+
+    @Test
+    void testScenarioAnalysis_useHashLookupFalse_skipsHashBenchmark() {
+        ScenarioAnalysisRequest.AssetScenario asset = buildSimpleAsset(1L, 
100.0, 1.0, 0.5, 0.5);
+        ScenarioAnalysisRequest req = new ScenarioAnalysisRequest();
+        req.setAssets(List.of(asset));
+        req.setUseHashLookup(false);
+
+        ScenarioAnalysisResponse resp = service.scenarioAnalysis(req);
+
+        assertEquals("SUCCESS", resp.getStatus());
+        assertEquals(0L, resp.getHashLookupUs(), "hash lookup time should be 0 
when disabled");
+    }
+
+    // ═══════════════════════════════════════════════════════════════════════
+    // scenarioAnalysis — validation
+    // ═══════════════════════════════════════════════════════════════════════
+
+    @Test
+    void testScenarioAnalysis_nullRequest_fails() {
+        ScenarioAnalysisResponse resp = service.scenarioAnalysis(null);
+        assertEquals("FAILED", resp.getStatus());
+    }
+
+    @Test
+    void testScenarioAnalysis_emptyAssets_fails() {
+        ScenarioAnalysisRequest req = new ScenarioAnalysisRequest();
+        req.setAssets(List.of());
+
+        ScenarioAnalysisResponse resp = service.scenarioAnalysis(req);
+        assertEquals("FAILED", resp.getStatus());
+        assertTrue(resp.getErrorMessage().contains("assets"),
+            "error must mention assets: " + resp.getErrorMessage());
+    }
+
+    @Test
+    void testScenarioAnalysis_probsDontSumToOne_fails() {
+        ScenarioAnalysisRequest.Scenario s1 = new 
ScenarioAnalysisRequest.Scenario();
+        s1.setPrice(110.0); s1.setProbability(0.4);
+        ScenarioAnalysisRequest.Scenario s2 = new 
ScenarioAnalysisRequest.Scenario();
+        s2.setPrice(90.0); s2.setProbability(0.4); // sum = 0.8, not 1.0
+
+        ScenarioAnalysisRequest.AssetScenario asset = new 
ScenarioAnalysisRequest.AssetScenario();
+        asset.setAssetId(1L);
+        asset.setCurrentPrice(100.0);
+        asset.setPositionSize(1.0);
+        asset.setScenarios(Arrays.asList(s1, s2));
+
+        ScenarioAnalysisRequest req = new ScenarioAnalysisRequest();
+        req.setAssets(List.of(asset));
+
+        ScenarioAnalysisResponse resp = service.scenarioAnalysis(req);
+        assertEquals("FAILED", resp.getStatus());
+        assertTrue(resp.getErrorMessage().contains("probabilities sum"),
+            "error must mention probability sum: " + resp.getErrorMessage());
+    }
+
+    @Test
+    void testScenarioAnalysis_customProbTolerance_acceptsSlightlyOff() {
+        // Probabilities sum to 0.999 — rejected with default 1e-4, accepted 
with 0.002
+        ScenarioAnalysisRequest.Scenario s1 = new 
ScenarioAnalysisRequest.Scenario();
+        s1.setPrice(110.0); s1.setProbability(0.5);
+        ScenarioAnalysisRequest.Scenario s2 = new 
ScenarioAnalysisRequest.Scenario();
+        s2.setPrice(90.0); s2.setProbability(0.499); // sum = 0.999
+
+        ScenarioAnalysisRequest.AssetScenario asset = new 
ScenarioAnalysisRequest.AssetScenario();
+        asset.setAssetId(1L);
+        asset.setCurrentPrice(100.0);
+        asset.setPositionSize(1.0);
+        asset.setScenarios(Arrays.asList(s1, s2));
+
+        // Default tolerance (1e-4) — should fail
+        ScenarioAnalysisRequest reqTight = new ScenarioAnalysisRequest();
+        reqTight.setAssets(List.of(asset));
+        assertEquals("FAILED", service.scenarioAnalysis(reqTight).getStatus(),
+            "tight tolerance should reject sum=0.999");
+
+        // Loose tolerance (0.002) — should succeed
+        ScenarioAnalysisRequest reqLoose = new ScenarioAnalysisRequest();
+        reqLoose.setAssets(List.of(asset));
+        reqLoose.setProbTolerance(0.002);
+        assertEquals("SUCCESS", service.scenarioAnalysis(reqLoose).getStatus(),
+            "loose tolerance should accept sum=0.999");
+    }
+
+    // ═══════════════════════════════════════════════════════════════════════
+    // MonteCarloRequest — getter defaults
+    // ═══════════════════════════════════════════════════════════════════════
+
+    @Test
+    void testMonteCarloRequest_getters_enforceDefaults() {
+        MonteCarloRequest req = new MonteCarloRequest();
+        req.setNSimulations(0);       // invalid → default 10,000
+        req.setNPeriods(-1);          // invalid → default 252
+        req.setInitialValue(0.0);     // invalid → default 1,000,000
+        req.setNPeriodsPerYear(0);    // invalid → default 252
+        req.setPercentiles(null);     // null → default {0.01, 0.05}
+
+        assertEquals(10_000, req.getNSimulations());
+        assertEquals(252, req.getNPeriods());
+        assertEquals(1_000_000.0, req.getInitialValue(), 1e-9);
+        assertEquals(252, req.getNPeriodsPerYear());
+        assertArrayEquals(new double[]{0.01, 0.05}, req.getPercentiles(), 
1e-9);
+    }
+
+    // ═══════════════════════════════════════════════════════════════════════
+    // Helper
+    // ═══════════════════════════════════════════════════════════════════════
+
+    private ScenarioAnalysisRequest.AssetScenario buildSimpleAsset(
+            long assetId, double currentPrice, double positionSize,
+            double prob1, double prob2) {
+        ScenarioAnalysisRequest.AssetScenario asset = new 
ScenarioAnalysisRequest.AssetScenario();
+        asset.setAssetId(assetId);
+        asset.setCurrentPrice(currentPrice);
+        asset.setPositionSize(positionSize);
+
+        ScenarioAnalysisRequest.Scenario s1 = new 
ScenarioAnalysisRequest.Scenario();
+        s1.setPrice(currentPrice * 1.1); s1.setProbability(prob1);
+        ScenarioAnalysisRequest.Scenario s2 = new 
ScenarioAnalysisRequest.Scenario();
+        s2.setPrice(currentPrice * 0.9); s2.setProbability(prob2);
+        asset.setScenarios(Arrays.asList(s1, s2));
+        return asset;
+    }
+
+    /** Expose initialValue for test assertions (not in response, but useful 
here). */
+    private double getInitialValue(MonteCarloRequest req) {
+        return req.getInitialValue();
+    }
+}


Reply via email to