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 a6ed1a4011398b5c26d040f5a1b522b2e8bb42c4
Author: Robert Lazarski <[email protected]>
AuthorDate: Mon Apr 6 13:46:46 2026 -1000

    springbootdemo-tomcat11: add Java financial benchmark service
    
    Java reference implementation of the Axis2/C financial benchmark service
    (axis-axis2-c-core/samples/user_guide/financial-benchmark-service).
    
    Provides identical API and financial calculations to the C service,
    enabling side-by-side performance comparison:
    - C on a $20 2GB Android phone: portfolioVariance 500 assets in ~5ms / 30MB
    - Java: requires 16-32GB JVM minimum to start
    
    Three operations (7 files):
    
    portfolioVariance — O(n²) covariance matrix multiplication
      σ²_p = Σ_i Σ_j w_i · w_j · σ_ij
      Accepts 2D double[][] or flat double[] covariance matrix.
      normalizeWeights: rescales weights to sum 1.0 (for unnormalized 
exposures).
      nPeriodsPerYear: controls annualized volatility (default 252).
      Reports weight_sum and weights_normalized for audit.
    
    monteCarlo — Geometric Brownian Motion VaR simulation
      S(t+dt) = S(t) × exp((μ − σ²/2)·dt + σ·√dt·Z)
      Uses Random.nextGaussian() (polar method).
      Seeded (randomSeed != 0) for reproducibility, unseeded for production.
      percentiles: caller-specified VaR levels, default [0.01, 0.05].
      Returns fixed var95/var99/cvar95 fields plus percentileVars list.
      nPeriodsPerYear: controls GBM time step dt = 1/nPeriodsPerYear.
    
    scenarioAnalysis — expected return + HashMap vs ArrayList benchmark
      E[r_i] = Σ_j p_j × (price_j / currentPrice − 1)
      probTolerance: configurable probability sum validation (default 1e-4).
      Mirrors DPT v2 Array→Map optimization for 500+ asset portfolios.
      Reports linear_us, hash_lookup_us, speedup, hash_build_us.
    
    API parity with C: all request/response field names match the C structs
    in financial_benchmark_service.h (camelCase vs snake_case only).
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
 .../webservices/FinancialBenchmarkService.java     | 526 +++++++++++++++++++++
 .../springboot/webservices/MonteCarloRequest.java  | 106 +++++
 .../springboot/webservices/MonteCarloResponse.java | 164 +++++++
 .../webservices/PortfolioVarianceRequest.java      | 104 ++++
 .../webservices/PortfolioVarianceResponse.java     | 118 +++++
 .../webservices/ScenarioAnalysisRequest.java       | 142 ++++++
 .../webservices/ScenarioAnalysisResponse.java      | 154 ++++++
 7 files changed, 1314 insertions(+)

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
new file mode 100644
index 0000000000..8482b3b47d
--- /dev/null
+++ 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/FinancialBenchmarkService.java
@@ -0,0 +1,526 @@
+/*
+ * 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.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.UUID;
+
+/**
+ * Java reference implementation of the Axis2/C Financial Benchmark Service.
+ *
+ * <p>Provides identical financial calculations and API to the C service in
+ * {@code axis-axis2-c-core/samples/user_guide/financial-benchmark-service},
+ * enabling side-by-side performance comparison on the same hardware:
+ *
+ * <table border="1">
+ *   <tr><th>Operation</th><th>Complexity</th><th>C edge-device (2 
GB)</th><th>Java enterprise minimum</th></tr>
+ *   <tr><td>portfolioVariance (500 assets)</td><td>O(n²)</td><td>~5 ms / 30 
MB</td><td>requires 16–32 GB JVM</td></tr>
+ *   <tr><td>monteCarlo (10k sims)</td><td>O(sims × periods)</td><td>~100 
ms</td><td>requires 16–32 GB JVM</td></tr>
+ *   <tr><td>scenarioAnalysis (1000 assets)</td><td>O(n) linear / O(1) 
hash</td><td>&lt;5 ms</td><td>requires 16–32 GB JVM</td></tr>
+ * </table>
+ *
+ * <h3>Operations</h3>
+ * <ul>
+ *   <li>{@link #portfolioVariance} — O(n²) covariance matrix: σ²_p = Σ_i Σ_j 
w_i·w_j·σ_ij</li>
+ *   <li>{@link #monteCarlo} — GBM simulation for VaR: S(t+dt) = 
S(t)·exp((μ−σ²/2)·dt + σ·√dt·Z)</li>
+ *   <li>{@link #scenarioAnalysis} — expected return + HashMap vs ArrayList 
lookup benchmark</li>
+ * </ul>
+ *
+ * <h3>API parity with C implementation</h3>
+ * All request/response fields match the C structs in {@code 
financial_benchmark_service.h}:
+ * normalizeWeights, nPeriodsPerYear, percentiles, probTolerance.
+ */
+@Component
+public class FinancialBenchmarkService {
+
+    private static final Logger logger = 
LogManager.getLogger(FinancialBenchmarkService.class);
+
+    /** Maximum assets accepted to bound memory allocation (matches C 
FINBENCH_MAX_ASSETS). */
+    public static final int MAX_ASSETS = 2_000;
+
+    /** Maximum Monte Carlo paths (matches C FINBENCH_MAX_SIMULATIONS). */
+    public static final int MAX_SIMULATIONS = 1_000_000;
+
+    /** Maximum scenario count per asset (matches C FINBENCH_MAX_SCENARIOS). */
+    public static final int MAX_SCENARIOS = 10;
+
+    /** Maximum number of percentile levels in a Monte Carlo request. */
+    public static final int MAX_PERCENTILES = 8;
+
+    // ── Portfolio Variance 
────────────────────────────────────────────────────
+
+    /**
+     * Calculates portfolio variance using O(n²) covariance matrix 
multiplication.
+     *
+     * <p>Formula: σ²_p = Σ_i Σ_j w_i · w_j · σ_ij
+     *
+     * <p>Input validation mirrors the C implementation: weight count must 
match
+     * n_assets, covariance matrix must be n×n, weights must sum to 1.0 (unless
+     * {@code normalizeWeights=true}).
+     */
+    public PortfolioVarianceResponse 
portfolioVariance(PortfolioVarianceRequest request) {
+        String uuid = UUID.randomUUID().toString();
+        String logPrefix = "FinancialBenchmarkService.portfolioVariance uuid=" 
+ uuid + " ";
+
+        if (request == null || request.getWeights() == null) {
+            return PortfolioVarianceResponse.failed(
+                "Missing required field: \"weights\" array (nAssets elements 
summing to 1.0).");
+        }
+
+        int n = request.getNAssets();
+        if (n <= 0 || n > MAX_ASSETS) {
+            return PortfolioVarianceResponse.failed(
+                "n_assets=" + n + " is out of range [1, " + MAX_ASSETS + "].");
+        }
+
+        // ── 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.");
+        }
+        if (cov.length != n) {
+            return PortfolioVarianceResponse.failed(
+                "covarianceMatrix row count " + cov.length + " != nAssets " + 
n + ".");
+        }
+        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 + ".");
+            }
+        }
+
+        // ── Weight validation / normalization 
─────────────────────────────────
+        double[] weights = Arrays.copyOf(request.getWeights(), n);
+        double weightSum = 0.0;
+        for (double w : weights) weightSum += w;
+
+        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.");
+            }
+            if (Math.abs(weightSum - 1.0) > 1e-10) {
+                for (int i = 0; i < n; i++) weights[i] /= weightSum;
+                weightsNormalized = true;
+                logger.info(logPrefix + "normalized weights (sum was " + 
weightSum + ")");
+            }
+        } else {
+            if (Math.abs(weightSum - 1.0) > 1e-4) {
+                return PortfolioVarianceResponse.failed(
+                    "weights sum to " + String.format("%.8f", weightSum) +
+                    ", expected 1.0 (tolerance 1e-4). " +
+                    "Pass normalizeWeights=true to rescale automatically.");
+            }
+        }
+
+        // ── O(n²) variance calculation 
────────────────────────────────────────
+        logger.info(logPrefix + "starting O(n²) variance for " + n + " 
assets");
+        long startNs = System.nanoTime();
+
+        double variance = 0.0;
+        long ops = 0;
+        for (int i = 0; i < n; i++) {
+            for (int j = 0; j < n; j++) {
+                variance += weights[i] * weights[j] * cov[i][j];
+                ops++;
+            }
+        }
+
+        long elapsedUs = (System.nanoTime() - startNs) / 1_000;
+
+        // Clamp negative variance from floating-point cancellation before sqrt
+        if (variance < 0.0) variance = 0.0;
+        double volatility = Math.sqrt(variance);
+        int npy = request.getNPeriodsPerYear();
+
+        PortfolioVarianceResponse response = new PortfolioVarianceResponse();
+        response.setStatus("SUCCESS");
+        response.setPortfolioVariance(variance);
+        response.setPortfolioVolatility(volatility);
+        response.setAnnualizedVolatility(volatility * Math.sqrt(npy));
+        response.setWeightSum(weightSum);
+        response.setWeightsNormalized(weightsNormalized);
+        response.setCalcTimeUs(elapsedUs);
+        response.setMatrixOperations(ops);
+        response.setOpsPerSecond(elapsedUs > 0 ? ops / (elapsedUs / 
1_000_000.0) : 0);
+        response.setMemoryUsedMb(heapUsedMb());
+        response.setRuntimeInfo(runtimeInfo());
+        if (request.getRequestId() != null) 
response.setRequestId(request.getRequestId());
+
+        logger.info(logPrefix + "completed: n=" + n + " variance=" +
+                String.format("%.6f", variance) + " elapsed=" + elapsedUs + 
"us ops=" + ops);
+        return response;
+    }
+
+    // ── Monte Carlo 
───────────────────────────────────────────────────────────
+
+    /**
+     * Runs a Monte Carlo VaR simulation using Geometric Brownian Motion.
+     *
+     * <p>S(t+dt) = S(t) × exp((μ − σ²/2)·dt + σ·√dt·Z), Z ~ N(0,1)
+     *
+     * <p>Uses {@link Random#nextGaussian()} (polar method) for normal 
variates.
+     * When {@code randomSeed != 0}, a seeded {@link Random} is used for 
reproducibility.
+     * When {@code randomSeed == 0}, a fresh unseeded instance gives 
non-deterministic results.
+     */
+    public MonteCarloResponse monteCarlo(MonteCarloRequest request) {
+        String uuid = UUID.randomUUID().toString();
+        String logPrefix = "FinancialBenchmarkService.monteCarlo uuid=" + uuid 
+ " ";
+
+        if (request == null) {
+            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;
+        double mu = request.getExpectedReturn();
+        double sigma = request.getVolatility();
+        int npy = request.getNPeriodsPerYear();
+
+        if (sigma < 0.0) {
+            return MonteCarloResponse.failed("volatility must be >= 0.");
+        }
+
+        // ── Pre-computed GBM constants 
────────────────────────────────────────
+        double dt = 1.0 / npy;
+        double drift = (mu - 0.5 * sigma * sigma) * dt;
+        double volSqrtDt = sigma * Math.sqrt(dt);
+
+        // ── PRNG: seeded for reproducibility, unseeded for production 
─────────
+        Random rng = request.getRandomSeed() != 0
+            ? new Random(request.getRandomSeed())
+            : new Random();
+
+        logger.info(logPrefix + "starting " + nSims + " sims × " + nPeriods + 
" periods" +
+                " (seed=" + request.getRandomSeed() + ", npy=" + npy + ")");
+
+        double[] finalValues = new double[nSims];
+        double sumFinal = 0.0;
+        double sumSqFinal = 0.0;
+        int profitCount = 0;
+        double maxDrawdown = 0.0;
+
+        long startNs = System.nanoTime();
+
+        for (int sim = 0; sim < nSims; sim++) {
+            double value = initialValue;
+            double peak = value;
+            double simMaxDrawdown = 0.0;
+
+            for (int period = 0; period < nPeriods; period++) {
+                double z = rng.nextGaussian();
+                value *= Math.exp(drift + volSqrtDt * z);
+
+                if (value > peak) {
+                    peak = value;
+                } else {
+                    double dd = (peak - value) / peak;
+                    if (dd > simMaxDrawdown) simMaxDrawdown = dd;
+                }
+            }
+
+            finalValues[sim] = value;
+            sumFinal += value;
+            sumSqFinal += value * value;
+            if (value > initialValue) profitCount++;
+            if (simMaxDrawdown > maxDrawdown) maxDrawdown = simMaxDrawdown;
+        }
+
+        long elapsedUs = (System.nanoTime() - startNs) / 1_000;
+
+        // ── Statistics 
────────────────────────────────────────────────────────
+        double mean = sumFinal / nSims;
+        double variance = (sumSqFinal / nSims) - (mean * mean);
+        if (variance < 0.0) variance = 0.0;
+
+        Arrays.sort(finalValues);
+
+        int idx5  = (int)(0.05 * nSims);
+        int idx1  = (int)(0.01 * nSims);
+        int idx50 = nSims / 2;
+
+        // CVaR: mean of worst 5%
+        double cvarSum = 0.0;
+        for (int i = 0; i < idx5; i++) cvarSum += finalValues[i];
+        double cvar95 = (idx5 > 0) ? (cvarSum / idx5) : finalValues[0];
+
+        // Caller-specified percentile VaR
+        List<MonteCarloResponse.PercentileVar> percentileVars = new 
ArrayList<>();
+        if (request.getPercentiles() != null) {
+            int maxPct = Math.min(request.getPercentiles().length, 
MAX_PERCENTILES);
+            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);
+                if (idx >= nSims) idx = nSims - 1;
+                percentileVars.add(new MonteCarloResponse.PercentileVar(
+                    p, initialValue - finalValues[idx]));
+            }
+        }
+
+        MonteCarloResponse response = new MonteCarloResponse();
+        response.setStatus("SUCCESS");
+        response.setMeanFinalValue(mean);
+        response.setMedianFinalValue(finalValues[idx50]);
+        response.setStdDevFinalValue(Math.sqrt(variance));
+        response.setVar95(initialValue - finalValues[idx5]);
+        response.setVar99(initialValue - finalValues[idx1]);
+        response.setCvar95(initialValue - cvar95);
+        response.setMaxDrawdown(maxDrawdown);
+        response.setProbProfit((double) profitCount / nSims);
+        response.setPercentileVars(percentileVars);
+        response.setCalcTimeUs(elapsedUs);
+        response.setSimulationsPerSecond(elapsedUs > 0 ? nSims / (elapsedUs / 
1_000_000.0) : 0);
+        response.setMemoryUsedMb(heapUsedMb());
+        if (request.getRequestId() != null) 
response.setRequestId(request.getRequestId());
+
+        logger.info(logPrefix + "completed: " + nSims + " sims in " + 
elapsedUs + "us" +
+                " VaR95=" + String.format("%.2f", response.getVar95()) +
+                " sims/sec=" + String.format("%.0f", 
response.getSimulationsPerSecond()));
+        return response;
+    }
+
+    // ── Scenario Analysis 
─────────────────────────────────────────────────────
+
+    /**
+     * Computes probability-weighted portfolio scenario analysis and benchmarks
+     * {@code HashMap} O(1) lookup against {@code ArrayList} O(n) linear scan.
+     *
+     * <p>Financial formulas per asset:
+     * <ul>
+     *   <li>E[r_i] = Σ_j p_j × (price_j / currentPrice − 1)</li>
+     *   <li>Upside_i = Σ_j p_j × max(0, price_j − currentPrice) × 
positionSize</li>
+     *   <li>Downside_i = Σ_j p_j × max(0, currentPrice − price_j) × 
positionSize</li>
+     * </ul>
+     * Portfolio E[r] = Σ_i (E[r_i] × positionValue_i) / Σ_i positionValue_i
+     */
+    public ScenarioAnalysisResponse scenarioAnalysis(ScenarioAnalysisRequest 
request) {
+        String uuid = UUID.randomUUID().toString();
+        String logPrefix = "FinancialBenchmarkService.scenarioAnalysis uuid=" 
+ uuid + " ";
+
+        if (request == null || request.getAssets() == null || 
request.getAssets().isEmpty()) {
+            return ScenarioAnalysisResponse.failed(
+                "Missing required field: \"assets\" array. " +
+                "Each entry must have assetId, currentPrice, positionSize, and 
scenarios " +
+                "[{price, probability}].");
+        }
+
+        List<ScenarioAnalysisRequest.AssetScenario> assets = 
request.getAssets();
+        int nAssets = assets.size();
+        if (nAssets > MAX_ASSETS) {
+            return ScenarioAnalysisResponse.failed(
+                "assets count " + nAssets + " exceeds maximum " + MAX_ASSETS + 
".");
+        }
+
+        double probTolerance = request.getProbTolerance();
+
+        // ── Step 1: Probability validation 
────────────────────────────────────
+        for (int i = 0; i < nAssets; i++) {
+            ScenarioAnalysisRequest.AssetScenario asset = assets.get(i);
+            if (asset.getScenarios() == null || 
asset.getScenarios().isEmpty()) continue;
+
+            double probSum = 0.0;
+            for (ScenarioAnalysisRequest.Scenario s : asset.getScenarios()) {
+                probSum += s.getProbability();
+            }
+            if (Math.abs(probSum - 1.0) > probTolerance) {
+                return ScenarioAnalysisResponse.failed(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()));
+            }
+        }
+
+        // ── Step 2: Financial computation 
─────────────────────────────────────
+        long calcStartNs = System.nanoTime();
+
+        double portfolioExpectedReturn = 0.0;
+        double totalPositionValue = 0.0;
+        double portfolioWeightedValue = 0.0;
+        double totalUpside = 0.0;
+        double totalDownside = 0.0;
+
+        for (ScenarioAnalysisRequest.AssetScenario asset : assets) {
+            if (asset.getCurrentPrice() <= 0.0 ||
+                    asset.getScenarios() == null || 
asset.getScenarios().isEmpty()) continue;
+
+            double assetExpectedReturn = 0.0;
+            double assetWeightedValue = 0.0;
+            double assetUpside = 0.0;
+            double assetDownside = 0.0;
+
+            for (ScenarioAnalysisRequest.Scenario scenario : 
asset.getScenarios()) {
+                double p = scenario.getProbability();
+                double price = scenario.getPrice();
+                double ret = (price / asset.getCurrentPrice()) - 1.0;
+
+                assetExpectedReturn += p * ret;
+                assetWeightedValue  += p * price * asset.getPositionSize();
+
+                if (price > asset.getCurrentPrice()) {
+                    assetUpside   += p * (price - asset.getCurrentPrice()) * 
asset.getPositionSize();
+                } else if (price < asset.getCurrentPrice()) {
+                    assetDownside += p * (asset.getCurrentPrice() - price) * 
asset.getPositionSize();
+                }
+            }
+
+            double positionValue = asset.getCurrentPrice() * 
asset.getPositionSize();
+            portfolioExpectedReturn += assetExpectedReturn * positionValue;
+            totalPositionValue      += positionValue;
+            portfolioWeightedValue  += assetWeightedValue;
+            totalUpside             += assetUpside;
+            totalDownside           += assetDownside;
+        }
+
+        if (totalPositionValue > 0.0) {
+            portfolioExpectedReturn /= totalPositionValue;
+        }
+
+        long calcElapsedUs = (System.nanoTime() - calcStartNs) / 1_000;
+
+        // ── Step 3: O(n) linear scan benchmark 
───────────────────────────────
+        int nLookups = nAssets * 10;
+        long linearStart = System.nanoTime();
+        long linearFound = 0;
+
+        for (int q = 0; q < nLookups; q++) {
+            long targetId = assets.get(q % nAssets).getAssetId();
+            for (ScenarioAnalysisRequest.AssetScenario asset : assets) {
+                if (asset.getAssetId() == targetId) {
+                    linearFound++;
+                    break;
+                }
+            }
+        }
+        long linearUs = (System.nanoTime() - linearStart) / 1_000;
+
+        // ── Step 4: O(1) HashMap benchmark 
────────────────────────────────────
+        long hashBuildUs = 0;
+        long hashLookupUs = 0;
+        long hashFound = 0;
+
+        if (request.isUseHashLookup()) {
+            long buildStart = System.nanoTime();
+            Map<Long, ScenarioAnalysisRequest.AssetScenario> hashMap =
+                new HashMap<>(nAssets * 2);
+            for (ScenarioAnalysisRequest.AssetScenario asset : assets) {
+                hashMap.put(asset.getAssetId(), asset);
+            }
+            hashBuildUs = (System.nanoTime() - buildStart) / 1_000;
+
+            long lookupStart = System.nanoTime();
+            for (int q = 0; q < nLookups; q++) {
+                long targetId = assets.get(q % nAssets).getAssetId();
+                if (hashMap.get(targetId) != null) hashFound++;
+            }
+            hashLookupUs = (System.nanoTime() - lookupStart) / 1_000;
+        }
+
+        // ── Build response 
─────────────────────────────────────────────────────
+        ScenarioAnalysisResponse response = new ScenarioAnalysisResponse();
+        response.setStatus("SUCCESS");
+        response.setExpectedReturn(portfolioExpectedReturn);
+        response.setWeightedValue(portfolioWeightedValue);
+        response.setUpsidePotential(totalUpside);
+        response.setDownsideRisk(totalDownside);
+        response.setUpsideDownsideRatio(
+            totalDownside > 0.0 ? totalUpside / totalDownside : 0.0);
+        response.setCalcTimeUs(calcElapsedUs);
+        response.setLinearLookupUs(linearUs);
+        response.setHashLookupUs(hashLookupUs);
+        response.setHashBuildUs(hashBuildUs);
+        response.setLookupSpeedup(
+            hashLookupUs > 0 ? (double) linearUs / hashLookupUs : Double.NaN);
+        response.setLookupsPerformed(nLookups);
+        response.setMemoryUsedMb(heapUsedMb());
+
+        String benchmarkSummary = String.format(
+            "linear_us=%d hash_lookup_us=%d speedup=%.1fx hash_build_us=%d " +
+            "(linear=%d found, hash=%d found, n_assets=%d, n_lookups=%d)",
+            linearUs, hashLookupUs,
+            hashLookupUs > 0 ? (double) linearUs / hashLookupUs : 0.0,
+            hashBuildUs, linearFound, hashFound, nAssets, nLookups);
+        response.setLookupBenchmark(benchmarkSummary);
+
+        if (request.getRequestId() != null) 
response.setRequestId(request.getRequestId());
+
+        logger.info(logPrefix + "completed: " + nAssets + " assets " +
+                nLookups + " lookups — linear=" + linearUs + "us " +
+                "hash_lookup=" + hashLookupUs + "us " +
+                "speedup=" + String.format("%.1f", 
response.getLookupSpeedup()) + "x " +
+                "E[r]=" + String.format("%.4f", portfolioExpectedReturn));
+        return response;
+    }
+
+    // ── Utilities 
─────────────────────────────────────────────────────────────
+
+    /**
+     * Resolves the covariance matrix from a request, preferring the 2D form.
+     * Returns null if neither form is present or if the flat array has wrong 
length.
+     */
+    private double[][] resolveCovarianceMatrix(PortfolioVarianceRequest 
request, int n) {
+        if (request.getCovarianceMatrix() != null) {
+            return request.getCovarianceMatrix();
+        }
+        double[] flat = request.getCovarianceMatrixFlat();
+        if (flat == null) return null;
+        if (flat.length != (long) n * n) return null;
+
+        double[][] matrix = new double[n][n];
+        for (int i = 0; i < n; i++) {
+            System.arraycopy(flat, i * n, matrix[i], 0, n);
+        }
+        return matrix;
+    }
+
+    /** JVM heap in use at call time, in MB. */
+    private long heapUsedMb() {
+        Runtime rt = Runtime.getRuntime();
+        return (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024);
+    }
+
+    /** Human-readable JVM / runtime identifier for response metadata. */
+    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));
+    }
+}
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
new file mode 100644
index 0000000000..9ea6ed8d5a
--- /dev/null
+++ 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/MonteCarloRequest.java
@@ -0,0 +1,106 @@
+/*
+ * 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;
+
+/**
+ * Request for Monte Carlo Value-at-Risk simulation.
+ *
+ * <p>Simulates portfolio value paths using 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>All fields have defaults so a minimal {@code {}} request body is valid.
+ *
+ * <h3>Example</h3>
+ * <pre>{@code
+ * {
+ *   "nSimulations": 50000,
+ *   "nPeriods": 252,
+ *   "initialValue": 1000000.0,
+ *   "expectedReturn": 0.08,
+ *   "volatility": 0.20,
+ *   "randomSeed": 42,
+ *   "percentiles": [0.01, 0.05, 0.10]
+ * }
+ * }</pre>
+ */
+public class MonteCarloRequest {
+
+    /** Number of simulation paths. Default: 10,000. Max: 1,000,000. */
+    private int nSimulations = 10_000;
+
+    /** Number of time steps per path (e.g., 252 trading days). Default: 252. 
*/
+    private int nPeriods = 252;
+
+    /** Initial portfolio value in currency units. Default: $1,000,000. */
+    private double initialValue = 1_000_000.0;
+
+    /** Expected annualized return (e.g., 0.08 for 8%). Default: 0.08. */
+    private double expectedReturn = 0.08;
+
+    /** Annualized volatility (e.g., 0.20 for 20%). Default: 0.20. */
+    private double volatility = 0.20;
+
+    /**
+     * Random seed for reproducibility. 0 (default) → non-deterministic.
+     * Seeded runs produce identical results across calls, enabling diff 
testing.
+     */
+    private long randomSeed = 0;
+
+    /**
+     * Trading periods per year. Controls GBM time step: dt = 
1/nPeriodsPerYear.
+     * Must match the frequency basis of {@code expectedReturn} and {@code 
volatility}
+     * (both must be annualized). Default: 252. Common alternatives: 260, 365, 
12.
+     */
+    private int nPeriodsPerYear = 252;
+
+    /**
+     * Percentile tail levels for VaR reporting. Values in (0, 1).
+     * Each entry p produces: VaR_p = initialValue − sorted_final_values[p × 
nSimulations].
+     * Default: [0.01, 0.05] (99% and 95% VaR). Up to 8 entries; extras are 
ignored.
+     */
+    private double[] percentiles = {0.01, 0.05};
+
+    /** Optional identifier echoed in the response for request tracing. */
+    private String requestId;
+
+    // ── getters 
──────────────────────────────────────────────────────────────
+
+    public int getNSimulations() { return nSimulations; }
+    public int getNPeriods() { return nPeriods; }
+    public double getInitialValue() { return initialValue; }
+    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 String getRequestId() { return requestId; }
+
+    // ── setters 
──────────────────────────────────────────────────────────────
+
+    public void setNSimulations(int nSimulations) { this.nSimulations = 
nSimulations; }
+    public void setNPeriods(int nPeriods) { this.nPeriods = nPeriods; }
+    public void setInitialValue(double initialValue) { this.initialValue = 
initialValue; }
+    public void setExpectedReturn(double expectedReturn) { this.expectedReturn 
= expectedReturn; }
+    public void setVolatility(double volatility) { this.volatility = 
volatility; }
+    public void setRandomSeed(long randomSeed) { this.randomSeed = randomSeed; 
}
+    public void setNPeriodsPerYear(int nPeriodsPerYear) { this.nPeriodsPerYear 
= nPeriodsPerYear; }
+    public void setPercentiles(double[] percentiles) { this.percentiles = 
percentiles; }
+    public void setRequestId(String requestId) { this.requestId = requestId; }
+}
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
new file mode 100644
index 0000000000..6750f807b9
--- /dev/null
+++ 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/MonteCarloResponse.java
@@ -0,0 +1,164 @@
+/*
+ * 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 java.util.List;
+
+/**
+ * Response for Monte Carlo Value-at-Risk simulation.
+ *
+ * <p>Fixed fields ({@code var95}, {@code var99}, {@code cvar95}) are always
+ * populated on success for backward compatibility. The {@code percentileVars}
+ * list reflects the caller-specified {@code percentiles} array.
+ */
+public class MonteCarloResponse {
+
+    private String status;
+    private String errorMessage;
+
+    // ── Distribution statistics 
───────────────────────────────────────────────
+
+    /** Mean of final portfolio values across all paths */
+    private double meanFinalValue;
+
+    /** Median (50th percentile) of final portfolio values */
+    private double medianFinalValue;
+
+    /** Standard deviation of final portfolio values */
+    private double stdDevFinalValue;
+
+    // ── Fixed VaR fields (always populated) 
──────────────────────────────────
+
+    /** Value at Risk at 95% confidence: initialValue − 5th-percentile final 
value */
+    private double var95;
+
+    /** Value at Risk at 99% confidence: initialValue − 1st-percentile final 
value */
+    private double var99;
+
+    /** Conditional VaR (Expected Shortfall) at 95%: initialValue − mean(worst 
5%) */
+    private double cvar95;
+
+    // ── Additional risk metrics 
───────────────────────────────────────────────
+
+    /** Maximum drawdown observed across all simulation paths (0–1 fraction) */
+    private double maxDrawdown;
+
+    /** Fraction of paths where final value > initial value */
+    private double probProfit;
+
+    // ── Caller-specified percentile VaR 
──────────────────────────────────────
+
+    /**
+     * VaR values for the percentile levels requested in {@code 
MonteCarloRequest.percentiles}.
+     * Each entry: {@code {"percentile": 0.01, "var": 185432.10}}.
+     */
+    private List<PercentileVar> percentileVars;
+
+    // ── Performance metrics 
───────────────────────────────────────────────────
+
+    /** Wall-clock time for the simulation in microseconds */
+    private long calcTimeUs;
+
+    /** Simulation throughput: nSimulations / (calcTimeUs / 1e6) */
+    private double simulationsPerSecond;
+
+    /** JVM heap used at response time in MB */
+    private long memoryUsedMb;
+
+    /** Echoed from request */
+    private String requestId;
+
+    // ── Inner types 
──────────────────────────────────────────────────────────
+
+    /**
+     * A single percentile VaR entry in {@code percentileVars}.
+     */
+    public static class PercentileVar {
+        private double percentile;
+        private double var;
+
+        public PercentileVar() {}
+
+        public PercentileVar(double percentile, double var) {
+            this.percentile = percentile;
+            this.var = var;
+        }
+
+        public double getPercentile() { return percentile; }
+        public void setPercentile(double percentile) { this.percentile = 
percentile; }
+        public double getVar() { return var; }
+        public void setVar(double var) { this.var = var; }
+    }
+
+    // ── Factory 
──────────────────────────────────────────────────────────────
+
+    public static MonteCarloResponse failed(String errorMessage) {
+        MonteCarloResponse r = new MonteCarloResponse();
+        r.status = "FAILED";
+        r.errorMessage = errorMessage;
+        return r;
+    }
+
+    // ── Getters / setters 
────────────────────────────────────────────────────
+
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+
+    public String getErrorMessage() { return errorMessage; }
+    public void setErrorMessage(String errorMessage) { this.errorMessage = 
errorMessage; }
+
+    public double getMeanFinalValue() { return meanFinalValue; }
+    public void setMeanFinalValue(double meanFinalValue) { this.meanFinalValue 
= meanFinalValue; }
+
+    public double getMedianFinalValue() { return medianFinalValue; }
+    public void setMedianFinalValue(double medianFinalValue) { 
this.medianFinalValue = medianFinalValue; }
+
+    public double getStdDevFinalValue() { return stdDevFinalValue; }
+    public void setStdDevFinalValue(double stdDevFinalValue) { 
this.stdDevFinalValue = stdDevFinalValue; }
+
+    public double getVar95() { return var95; }
+    public void setVar95(double var95) { this.var95 = var95; }
+
+    public double getVar99() { return var99; }
+    public void setVar99(double var99) { this.var99 = var99; }
+
+    public double getCvar95() { return cvar95; }
+    public void setCvar95(double cvar95) { this.cvar95 = cvar95; }
+
+    public double getMaxDrawdown() { return maxDrawdown; }
+    public void setMaxDrawdown(double maxDrawdown) { this.maxDrawdown = 
maxDrawdown; }
+
+    public double getProbProfit() { return probProfit; }
+    public void setProbProfit(double probProfit) { this.probProfit = 
probProfit; }
+
+    public List<PercentileVar> getPercentileVars() { return percentileVars; }
+    public void setPercentileVars(List<PercentileVar> percentileVars) { 
this.percentileVars = percentileVars; }
+
+    public long getCalcTimeUs() { return calcTimeUs; }
+    public void setCalcTimeUs(long calcTimeUs) { this.calcTimeUs = calcTimeUs; 
}
+
+    public double getSimulationsPerSecond() { return simulationsPerSecond; }
+    public void setSimulationsPerSecond(double simulationsPerSecond) { 
this.simulationsPerSecond = simulationsPerSecond; }
+
+    public long getMemoryUsedMb() { return memoryUsedMb; }
+    public void setMemoryUsedMb(long memoryUsedMb) { this.memoryUsedMb = 
memoryUsedMb; }
+
+    public String getRequestId() { return requestId; }
+    public void setRequestId(String requestId) { this.requestId = requestId; }
+}
diff --git 
a/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/PortfolioVarianceRequest.java
 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/PortfolioVarianceRequest.java
new file mode 100644
index 0000000000..5c42b5cf65
--- /dev/null
+++ 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/PortfolioVarianceRequest.java
@@ -0,0 +1,104 @@
+/*
+ * 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;
+
+/**
+ * Request for portfolio variance calculation.
+ *
+ * <p>Computes σ²_p = Σ_i Σ_j w_i · w_j · σ_ij — an O(n²) operation
+ * that mirrors correlation/risk calculations in DPT v2 and similar systems.
+ *
+ * <h3>Covariance matrix formats</h3>
+ * <ul>
+ *   <li><b>2D array</b> (preferred): {@code covarianceMatrix[i][j]} — natural 
JSON nested array</li>
+ *   <li><b>Flat array</b> (alternative): {@code covarianceMatrixFlat} of 
length n² in row-major order</li>
+ * </ul>
+ * If both are supplied, {@code covarianceMatrix} takes precedence.
+ *
+ * <h3>Example</h3>
+ * <pre>{@code
+ * {
+ *   "weights": [0.4, 0.6],
+ *   "covarianceMatrix": [[0.04, 0.006], [0.006, 0.09]],
+ *   "normalizeWeights": false,
+ *   "nPeriodsPerYear": 252
+ * }
+ * }</pre>
+ */
+public class PortfolioVarianceRequest {
+
+    /** Portfolio weights. Length determines n_assets when nAssets is not set. 
*/
+    private double[] weights;
+
+    /**
+     * Covariance matrix in 2D format: {@code covarianceMatrix[i][j]}.
+     * Takes precedence over {@code covarianceMatrixFlat} if both are provided.
+     */
+    private double[][] covarianceMatrix;
+
+    /**
+     * Covariance matrix in flat row-major format: element (i,j) is at index
+     * {@code i * nAssets + j}. Length must be nAssets². Used when the caller
+     * cannot produce a nested JSON array (e.g., numpy {@code .flatten()}).
+     */
+    private double[] covarianceMatrixFlat;
+
+    /**
+     * When {@code true}, weights are rescaled to sum to 1.0 before computing
+     * variance. Allows callers to pass unnormalized exposures (e.g., notional
+     * position values) without a client-side preprocessing step.
+     * When {@code false} (default), weights that deviate from 1.0 by more than
+     * 1e-4 return an error.
+     */
+    private boolean normalizeWeights = false;
+
+    /**
+     * Trading periods per year used to annualize volatility.
+     * {@code annualizedVolatility = portfolioVolatility × 
sqrt(nPeriodsPerYear)}.
+     * Common values: 252 (equity, default), 260 (some fixed-income 
conventions),
+     * 365 (crypto), 12 (monthly factor models).
+     */
+    private int nPeriodsPerYear = 252;
+
+    /** Optional identifier echoed in the response for request tracing. */
+    private String requestId;
+
+    // ── getters 
──────────────────────────────────────────────────────────────
+
+    public double[] getWeights() { return weights; }
+    public double[][] getCovarianceMatrix() { return covarianceMatrix; }
+    public double[] getCovarianceMatrixFlat() { return covarianceMatrixFlat; }
+    public boolean isNormalizeWeights() { return normalizeWeights; }
+    public int getNPeriodsPerYear() { return nPeriodsPerYear > 0 ? 
nPeriodsPerYear : 252; }
+    public String getRequestId() { return requestId; }
+
+    // ── setters 
──────────────────────────────────────────────────────────────
+
+    public void setWeights(double[] weights) { this.weights = weights; }
+    public void setCovarianceMatrix(double[][] covarianceMatrix) { 
this.covarianceMatrix = covarianceMatrix; }
+    public void setCovarianceMatrixFlat(double[] covarianceMatrixFlat) { 
this.covarianceMatrixFlat = covarianceMatrixFlat; }
+    public void setNormalizeWeights(boolean normalizeWeights) { 
this.normalizeWeights = normalizeWeights; }
+    public void setNPeriodsPerYear(int nPeriodsPerYear) { this.nPeriodsPerYear 
= nPeriodsPerYear; }
+    public void setRequestId(String requestId) { this.requestId = requestId; }
+
+    /** Derived: number of assets inferred from weights array length. */
+    public int getNAssets() {
+        return weights != null ? weights.length : 0;
+    }
+}
diff --git 
a/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/PortfolioVarianceResponse.java
 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/PortfolioVarianceResponse.java
new file mode 100644
index 0000000000..a720c18f32
--- /dev/null
+++ 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/PortfolioVarianceResponse.java
@@ -0,0 +1,118 @@
+/*
+ * 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;
+
+/**
+ * Response for portfolio variance calculation.
+ *
+ * <p>Contains the computed variance, volatility, and performance metrics.
+ * When {@code status == "FAILED"}, only {@code errorMessage} is meaningful.
+ */
+public class PortfolioVarianceResponse {
+
+    private String status;
+    private String errorMessage;
+
+    // ── Results 
──────────────────────────────────────────────────────────────
+
+    /** Portfolio variance σ²_p = Σ_i Σ_j w_i · w_j · σ_ij */
+    private double portfolioVariance;
+
+    /** Portfolio volatility σ = sqrt(σ²_p) */
+    private double portfolioVolatility;
+
+    /** Annualized volatility σ × sqrt(nPeriodsPerYear) */
+    private double annualizedVolatility;
+
+    /** Actual sum of weights as received (before normalization if applied) */
+    private double weightSum;
+
+    /** True when weights were rescaled to sum to 1.0 (normalizeWeights=true 
was effective) */
+    private boolean weightsNormalized;
+
+    // ── Performance metrics 
───────────────────────────────────────────────────
+
+    /** Wall-clock time for the O(n²) calculation in microseconds */
+    private long calcTimeUs;
+
+    /** Number of multiply-add operations: n_assets² */
+    private long matrixOperations;
+
+    /** Throughput: matrixOperations / (calcTimeUs / 1e6) */
+    private double opsPerSecond;
+
+    /** JVM heap used at response time in MB */
+    private long memoryUsedMb;
+
+    /** Runtime identifier (JVM version, heap config) */
+    private String runtimeInfo;
+
+    /** Echoed from request */
+    private String requestId;
+
+    // ── Constructors 
─────────────────────────────────────────────────────────
+
+    public static PortfolioVarianceResponse failed(String errorMessage) {
+        PortfolioVarianceResponse r = new PortfolioVarianceResponse();
+        r.status = "FAILED";
+        r.errorMessage = errorMessage;
+        return r;
+    }
+
+    // ── Getters / setters 
────────────────────────────────────────────────────
+
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+
+    public String getErrorMessage() { return errorMessage; }
+    public void setErrorMessage(String errorMessage) { this.errorMessage = 
errorMessage; }
+
+    public double getPortfolioVariance() { return portfolioVariance; }
+    public void setPortfolioVariance(double portfolioVariance) { 
this.portfolioVariance = portfolioVariance; }
+
+    public double getPortfolioVolatility() { return portfolioVolatility; }
+    public void setPortfolioVolatility(double portfolioVolatility) { 
this.portfolioVolatility = portfolioVolatility; }
+
+    public double getAnnualizedVolatility() { return annualizedVolatility; }
+    public void setAnnualizedVolatility(double annualizedVolatility) { 
this.annualizedVolatility = annualizedVolatility; }
+
+    public double getWeightSum() { return weightSum; }
+    public void setWeightSum(double weightSum) { this.weightSum = weightSum; }
+
+    public boolean isWeightsNormalized() { return weightsNormalized; }
+    public void setWeightsNormalized(boolean weightsNormalized) { 
this.weightsNormalized = weightsNormalized; }
+
+    public long getCalcTimeUs() { return calcTimeUs; }
+    public void setCalcTimeUs(long calcTimeUs) { this.calcTimeUs = calcTimeUs; 
}
+
+    public long getMatrixOperations() { return matrixOperations; }
+    public void setMatrixOperations(long matrixOperations) { 
this.matrixOperations = matrixOperations; }
+
+    public double getOpsPerSecond() { return opsPerSecond; }
+    public void setOpsPerSecond(double opsPerSecond) { this.opsPerSecond = 
opsPerSecond; }
+
+    public long getMemoryUsedMb() { return memoryUsedMb; }
+    public void setMemoryUsedMb(long memoryUsedMb) { this.memoryUsedMb = 
memoryUsedMb; }
+
+    public String getRuntimeInfo() { return runtimeInfo; }
+    public void setRuntimeInfo(String runtimeInfo) { this.runtimeInfo = 
runtimeInfo; }
+
+    public String getRequestId() { return requestId; }
+    public void setRequestId(String requestId) { this.requestId = requestId; }
+}
diff --git 
a/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/ScenarioAnalysisRequest.java
 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/ScenarioAnalysisRequest.java
new file mode 100644
index 0000000000..a00c57475e
--- /dev/null
+++ 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/ScenarioAnalysisRequest.java
@@ -0,0 +1,142 @@
+/*
+ * 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 java.util.List;
+
+/**
+ * Request for scenario analysis and hash-vs-linear lookup benchmark.
+ *
+ * <p>Computes probability-weighted expected return, upside, and downside
+ * for a portfolio under multiple price scenarios. Also benchmarks
+ * {@code HashMap} O(1) lookup against {@code ArrayList} O(n) scan,
+ * mirroring the Array→Map optimization used in DPT v2 for 500+ asset 
portfolios.
+ *
+ * <h3>Example</h3>
+ * <pre>{@code
+ * {
+ *   "assets": [{
+ *     "assetId": 1001,
+ *     "currentPrice": 150.00,
+ *     "positionSize": 100.0,
+ *     "scenarios": [
+ *       {"price": 165.0, "probability": 0.40},
+ *       {"price": 150.0, "probability": 0.35},
+ *       {"price": 130.0, "probability": 0.25}
+ *     ]
+ *   }],
+ *   "useHashLookup": true,
+ *   "probTolerance": 0.0001
+ * }
+ * }</pre>
+ */
+public class ScenarioAnalysisRequest {
+
+    /** List of assets with scenario prices and probabilities. Required. */
+    private List<AssetScenario> assets;
+
+    /**
+     * When {@code true} (default), the service benchmarks both {@code HashMap}
+     * and {@code ArrayList} lookups and reports the speedup ratio.
+     * When {@code false}, only the linear scan is timed.
+     */
+    private boolean useHashLookup = true;
+
+    /**
+     * Tolerance for probability sum validation per asset.
+     * Each asset's scenario probabilities must sum to 1.0 within this 
tolerance.
+     * Default: 1e-4 (0.01%). Pass 0.0 to keep the default. Clamped to [1e-10, 
0.1].
+     * Loosen (e.g., 0.001) when aggregating externally-sourced probabilities
+     * that carry rounding error; keep tight to catch genuinely miscounted 
scenarios.
+     */
+    private double probTolerance = 1e-4;
+
+    /** Optional identifier echoed in the response for request tracing. */
+    private String requestId;
+
+    // ── Inner types 
──────────────────────────────────────────────────────────
+
+    /**
+     * A single asset in the portfolio with associated scenario data.
+     */
+    public static class AssetScenario {
+
+        /** Unique asset identifier (e.g., security ID or fund-asset ID). */
+        private long assetId;
+
+        /** Current market price in currency units. Must be > 0. */
+        private double currentPrice;
+
+        /** Position size in shares/units. Used to scale upside/downside to 
dollar terms. */
+        private double positionSize;
+
+        /**
+         * Scenario outcomes. Probabilities must sum to 1.0 (within {@code 
probTolerance}).
+         * Up to {@link FinancialBenchmarkService#MAX_SCENARIOS} entries.
+         */
+        private List<Scenario> scenarios;
+
+        public long getAssetId() { return assetId; }
+        public void setAssetId(long assetId) { this.assetId = assetId; }
+
+        public double getCurrentPrice() { return currentPrice; }
+        public void setCurrentPrice(double currentPrice) { this.currentPrice = 
currentPrice; }
+
+        public double getPositionSize() { return positionSize; }
+        public void setPositionSize(double positionSize) { this.positionSize = 
positionSize; }
+
+        public List<Scenario> getScenarios() { return scenarios; }
+        public void setScenarios(List<Scenario> scenarios) { this.scenarios = 
scenarios; }
+    }
+
+    /**
+     * A single price scenario for an asset.
+     */
+    public static class Scenario {
+
+        /** Target price in this scenario (currency units). */
+        private double price;
+
+        /** Probability weight in [0, 1]. All scenarios for an asset must sum 
to 1.0. */
+        private double probability;
+
+        public double getPrice() { return price; }
+        public void setPrice(double price) { this.price = price; }
+
+        public double getProbability() { return probability; }
+        public void setProbability(double probability) { this.probability = 
probability; }
+    }
+
+    // ── Getters / setters 
────────────────────────────────────────────────────
+
+    public List<AssetScenario> getAssets() { return assets; }
+    public void setAssets(List<AssetScenario> assets) { this.assets = assets; }
+
+    public boolean isUseHashLookup() { return useHashLookup; }
+    public void setUseHashLookup(boolean useHashLookup) { this.useHashLookup = 
useHashLookup; }
+
+    public double getProbTolerance() {
+        if (probTolerance <= 0.0) return 1e-4;
+        return Math.min(probTolerance, 0.1);
+    }
+    public void setProbTolerance(double probTolerance) { this.probTolerance = 
probTolerance; }
+
+    public String getRequestId() { return requestId; }
+    public void setRequestId(String requestId) { this.requestId = requestId; }
+}
diff --git 
a/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/ScenarioAnalysisResponse.java
 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/ScenarioAnalysisResponse.java
new file mode 100644
index 0000000000..afe8ebc0d5
--- /dev/null
+++ 
b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/webservices/ScenarioAnalysisResponse.java
@@ -0,0 +1,154 @@
+/*
+ * 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;
+
+/**
+ * Response for scenario analysis and hash-vs-linear lookup benchmark.
+ *
+ * <p>Financial results (expected return, upside, downside) are always 
computed.
+ * Benchmark fields ({@code hashLookupUs}, {@code linearLookupUs}, {@code 
lookupSpeedup})
+ * are populated when {@code useHashLookup=true}.
+ */
+public class ScenarioAnalysisResponse {
+
+    private String status;
+    private String errorMessage;
+
+    // ── Financial results 
─────────────────────────────────────────────────────
+
+    /**
+     * Portfolio-level expected return: position-value-weighted average of
+     * per-asset expected returns. E[r_i] = Σ_j (p_j × (price_j / currentPrice 
- 1)).
+     */
+    private double expectedReturn;
+
+    /**
+     * Probability-weighted portfolio value: Σ_asset Σ_scenario (p_j × price_j 
× positionSize).
+     */
+    private double weightedValue;
+
+    /**
+     * Upside potential in currency units: Σ (p_j × max(0, price_j − 
currentPrice) × positionSize).
+     */
+    private double upsidePotential;
+
+    /**
+     * Downside risk in currency units: Σ (p_j × max(0, currentPrice − 
price_j) × positionSize).
+     */
+    private double downsideRisk;
+
+    /**
+     * Upside/downside ratio. 0 when downsideRisk == 0 (no loss scenarios).
+     * > 1 means more expected upside than downside.
+     */
+    private double upsideDownsideRatio;
+
+    // ── Benchmark results 
─────────────────────────────────────────────────────
+
+    /** Wall-clock time for O(n) linear scan benchmark in microseconds */
+    private long linearLookupUs;
+
+    /** Wall-clock time for O(1) HashMap lookup benchmark in microseconds */
+    private long hashLookupUs;
+
+    /** Wall-clock time to build the HashMap in microseconds (amortized in 
real workloads) */
+    private long hashBuildUs;
+
+    /** Speedup: linearLookupUs / hashLookupUs. NaN when hash benchmark was 
skipped. */
+    private double lookupSpeedup;
+
+    /** Total lookup operations performed in each benchmark */
+    private int lookupsPerformed;
+
+    /**
+     * Human-readable benchmark summary:
+     * linear/hash times, speedup, found counts, asset and lookup counts.
+     */
+    private String lookupBenchmark;
+
+    // ── Performance metadata 
──────────────────────────────────────────────────
+
+    /** Wall-clock time for the financial computation (separate from lookup 
benchmark) */
+    private long calcTimeUs;
+
+    /** JVM heap used at response time in MB */
+    private long memoryUsedMb;
+
+    /** Echoed from request */
+    private String requestId;
+
+    // ── Factory 
──────────────────────────────────────────────────────────────
+
+    public static ScenarioAnalysisResponse failed(String errorMessage) {
+        ScenarioAnalysisResponse r = new ScenarioAnalysisResponse();
+        r.status = "FAILED";
+        r.errorMessage = errorMessage;
+        return r;
+    }
+
+    // ── Getters / setters 
────────────────────────────────────────────────────
+
+    public String getStatus() { return status; }
+    public void setStatus(String status) { this.status = status; }
+
+    public String getErrorMessage() { return errorMessage; }
+    public void setErrorMessage(String errorMessage) { this.errorMessage = 
errorMessage; }
+
+    public double getExpectedReturn() { return expectedReturn; }
+    public void setExpectedReturn(double expectedReturn) { this.expectedReturn 
= expectedReturn; }
+
+    public double getWeightedValue() { return weightedValue; }
+    public void setWeightedValue(double weightedValue) { this.weightedValue = 
weightedValue; }
+
+    public double getUpsidePotential() { return upsidePotential; }
+    public void setUpsidePotential(double upsidePotential) { 
this.upsidePotential = upsidePotential; }
+
+    public double getDownsideRisk() { return downsideRisk; }
+    public void setDownsideRisk(double downsideRisk) { this.downsideRisk = 
downsideRisk; }
+
+    public double getUpsideDownsideRatio() { return upsideDownsideRatio; }
+    public void setUpsideDownsideRatio(double upsideDownsideRatio) { 
this.upsideDownsideRatio = upsideDownsideRatio; }
+
+    public long getLinearLookupUs() { return linearLookupUs; }
+    public void setLinearLookupUs(long linearLookupUs) { this.linearLookupUs = 
linearLookupUs; }
+
+    public long getHashLookupUs() { return hashLookupUs; }
+    public void setHashLookupUs(long hashLookupUs) { this.hashLookupUs = 
hashLookupUs; }
+
+    public long getHashBuildUs() { return hashBuildUs; }
+    public void setHashBuildUs(long hashBuildUs) { this.hashBuildUs = 
hashBuildUs; }
+
+    public double getLookupSpeedup() { return lookupSpeedup; }
+    public void setLookupSpeedup(double lookupSpeedup) { this.lookupSpeedup = 
lookupSpeedup; }
+
+    public int getLookupsPerformed() { return lookupsPerformed; }
+    public void setLookupsPerformed(int lookupsPerformed) { 
this.lookupsPerformed = lookupsPerformed; }
+
+    public String getLookupBenchmark() { return lookupBenchmark; }
+    public void setLookupBenchmark(String lookupBenchmark) { 
this.lookupBenchmark = lookupBenchmark; }
+
+    public long getCalcTimeUs() { return calcTimeUs; }
+    public void setCalcTimeUs(long calcTimeUs) { this.calcTimeUs = calcTimeUs; 
}
+
+    public long getMemoryUsedMb() { return memoryUsedMb; }
+    public void setMemoryUsedMb(long memoryUsedMb) { this.memoryUsedMb = 
memoryUsedMb; }
+
+    public String getRequestId() { return requestId; }
+    public void setRequestId(String requestId) { this.requestId = requestId; }
+}

Reply via email to