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><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; } +}
