You are viewing a plain text version of this content. The canonical link for it is here.
Posted to c-commits@axis.apache.org by ro...@apache.org on 2026/09/20 15:58:10 UTC
(axis-axis2-c-core) 02/07: Add composeCovariance to the financial benchmark sample
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-c-core.git
commit bc641a62361e1a1fa30c90c114aaf960e74cba34
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Sat Sep 19 14:33:33 2026 -1000
Add composeCovariance to the financial benchmark sample
Builds Sigma = D*R*D from per-asset volatilities and either a full
correlation matrix or one uniform rho, then Cholesky-checks it and refuses
a matrix that is not positive definite, naming the failing index. This is
the validated front door to portfolioVariance, which by design accepts any
matrix. Wired through the handler, the internal dispatcher, the MCP tool
catalog, services.xml, metadata and docs. The fuzz harness gains an
exercise for it and its stale function names now match the header.
Co-Authored-By: Claude Fable 5.1 <no...@anthropic.com>
---
docs/HTTP2_ANDROID.md | 42 +-
docs/MCP_EXAMPLES.md | 58 ++
fuzz/fuzz_finbench.c | 44 +-
.../financial-benchmark-service/README.md | 60 +-
.../financial-benchmark-service/services.xml | 70 ++-
.../src/financial_benchmark_service.c | 604 +++++++++++++++++++++
.../src/financial_benchmark_service.h | 201 +++++++
.../src/financial_benchmark_service_handler.c | 6 +
.../financial-benchmark-service/src/finbench_mcp.c | 43 +-
.../financial-benchmark-service/src/finbench_mcp.h | 5 +-
10 files changed, 1117 insertions(+), 16 deletions(-)
diff --git a/docs/HTTP2_ANDROID.md b/docs/HTTP2_ANDROID.md
index 973a8f85c..68317412f 100644
--- a/docs/HTTP2_ANDROID.md
+++ b/docs/HTTP2_ANDROID.md
@@ -554,6 +554,43 @@ Verify afterwards with `nm -D` that the service symbol is `T` and not `W`
Deploy to `services/MyNewService/services.xml` in the Axis2 repository.
+## Adding an Operation to a Registered Service
+
+The registry is keyed by **service**, not by operation. One weak symbol and
+one `strcmp` in `android_static_service_lookup()` cover every operation the
+service will ever have, because the engine hands the whole JSON request to the
+service's single `_invoke_json` entry point and the service routes internally
+(by the `action` field, or by request shape when the URL path is unavailable).
+So a new operation on an already-registered service touches **nothing in this
+repository's registry**. What it does touch, using `composeCovariance` on
+`FinancialBenchmarkService` as the worked example:
+
+| Where | Change |
+|---|---|
+| `financial_benchmark_service.h` / `.c` | The request and response structs, the `*_json_only()` entry point, and a branch in the internal `finbench_dispatch_json_obj()` (action name and, for the Android path, a request-shape rule: `composeCovariance` is the only operation that carries `volatilities`) |
+| `financial_benchmark_service_handler.c` | A branch in `route_operation()` for the server-side URL-path dispatch |
+| `finbench_mcp.c` | The tool's `inputSchema` constant, a row in the tool table, and a branch in `tools/call` |
+| `services.xml` (upstream and in the app) | An `<operation>` element; on Android the `RESTLocation` is what maps the URL path to the operation |
+| `finbench_get_metadata_json()` | The operations list |
+| The application's adapter | If it routes by request shape rather than by `action`, one more rule. The Kanaha Calcs adapter adds `volatilities → composeCovariance` |
+| `fuzz/fuzz_finbench.c` | An exercise block for the new parser and compute path |
+
+Two things are easy to get wrong:
+
+- **The request-shape rule must be unambiguous.** The Android adapter cannot
+ see the URL, so it infers the operation from field names. A new operation
+ must own a field no other operation uses, or its requests will be routed to
+ the wrong one and answered with that operation's "missing field" error.
+- **Both link lines in the application build.** The httpd and the MCP binary
+ are linked separately; the operation lives in
+ `financial_benchmark_service.o`, which both already include, so no link
+ change was needed here. A new operation that adds a source file has to be
+ added to both.
+
+Verify without the device: after building, `nm -D libyour_httpd.so | grep
+<new>_json_only` should show a `T`, and `tools/list` from the rebuilt MCP
+binary should list the tool.
+
## mod_axis2 Configuration
### Android-Specific Initialization
@@ -775,6 +812,7 @@ are permissive (MIT + Apache 2.0), its patterns can flow upstream to Apache proj
|---------|-------------|---------|------------|
| CameraControlService | [Kanaha Camera](https://github.com/robertlazarski/kanaha) | GPL v3+ | fork/execvp Intent IPC (GPL boundary) |
| AudioSearchService | [Kanaha Audio](https://github.com/robertlazarski/kanaha-audio) | Apache 2.0 | Direct function call (no GPL boundary) |
+| FinancialBenchmarkService | [Kanaha Calcs](https://github.com/robertlazarski/kanaha-calcs) (service source is the upstream sample under `samples/user_guide/financial-benchmark-service`) | Apache 2.0 | Direct function call; the app supplies only the two-argument adapter |
## Summary Table
@@ -795,7 +833,7 @@ Key files for Android support:
**Axis2/C Core:**
- `configure.ac` - Android detection, C flags
-- `src/core/receivers/axis2_json_rpc_msg_recv.c` - Static service registry (CameraControlService, AudioSearchService)
+- `src/core/receivers/axis2_json_rpc_msg_recv.c` - Static service registry (CameraControlService, AudioSearchService, FinancialBenchmarkService)
- `src/core/transport/http/server/apache2/mod_axis2.c` - Android init, logging
- `src/core/transport/http/server/apache2/apache2_worker.c` - Request processing
@@ -814,4 +852,4 @@ Key files for Android support:
---
-*Last updated: 2026-01-14*
+*Last updated: 2026-09-19*
diff --git a/docs/MCP_EXAMPLES.md b/docs/MCP_EXAMPLES.md
index 4bbe86474..ec43bd1f8 100644
--- a/docs/MCP_EXAMPLES.md
+++ b/docs/MCP_EXAMPLES.md
@@ -198,6 +198,63 @@ memory (RSS under load).
---
+### Compose Covariance — Σ = D·R·D with a Cholesky Check
+
+`portfolioVariance` takes a covariance matrix and, by design, does not check
+that it is positive semi-definite. `composeCovariance` is the validated way to
+build one: per-asset volatilities plus either a full correlation matrix or one
+uniform correlation for every pair. It refuses anything that fails Cholesky.
+
+```bash
+# The "all correlations spike to 0.8" stress case from the demos above, built
+# from the five vols instead of typed as 25 covariances
+curl -sk --http2 -X POST -H 'Content-Type: application/json' \
+ -d '{"volatilities":[0.243,0.244,0.314,0.219,0.167],"correlation":0.8,
+ "asset_ids":["MSFT","AAPL","AMZN","JPM","JNJ"]}' \
+ https://10.10.10.10/services/FinancialBenchmarkService/composeCovariance
+```
+
+```json
+{
+ "status": "SUCCESS",
+ "n_assets": 5,
+ "covariance_matrix": [0.059049, 0.047434, 0.061042, 0.042574, 0.032465, 0.047434, 0.059536, ...],
+ "correlation_matrix": [1.0, 0.8, 0.8, 0.8, 0.8, 0.8, 1.0, ...],
+ "volatilities": [0.243, 0.244, 0.314, 0.219, 0.167],
+ "positive_definite": true,
+ "positive_definite_checked": true,
+ "cholesky_failed_at": -1,
+ "min_pivot": 0.006890,
+ "calc_time_us": 3,
+ "asset_ids": ["MSFT", "AAPL", "AMZN", "JPM", "JNJ"]
+}
+```
+
+Pass that `covariance_matrix` straight to `portfolioVariance` with
+`"n_periods_per_year": 1` (the vols were annualized, so Σ is): the weights
+`[0.25, 0.25, 0.20, 0.15, 0.15]` give volatility 0.2228, matching the stress
+result in Demo 1. The same vols with the historical correlation matrix give
+0.1567.
+
+A uniform correlation that cannot be positive definite is refused with the
+bound stated, not silently clamped:
+
+```bash
+echo '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"composeCovariance","arguments":{"volatilities":[0.243,0.244,0.314,0.219,0.167],"correlation":-0.5}}}' \
+ | financial-benchmark-mcp
+```
+
+```json
+{"status":"FAILED","n_assets":5,"positive_definite":false,"positive_definite_checked":true,
+ "cholesky_failed_at":2,"min_pivot":0.0,
+ "error_message":"Not positive definite: Cholesky failed at index 2. With 5 assets a uniform correlation must satisfy -0.25 < rho < 1; got -0.5."}
+```
+
+Every other refusal names its field the same way: a vol that is not > 0, a
+correlation outside [−1, 1], an asymmetric or non-unit-diagonal matrix, or
+both forms supplied at once. Vols and correlations are short, bounded and
+checkable; that is why this is the front door and the raw matrix is not.
+
### Monte Carlo Value at Risk — Geometric Brownian Motion
Monte Carlo VaR is the standard approach for estimating portfolio loss at a
@@ -382,6 +439,7 @@ curl -k --http2 -s \
"version": "1.0.0",
"operations": [
"portfolioVariance",
+ "composeCovariance",
"monteCarlo",
"scenarioAnalysis",
"metadata"
diff --git a/fuzz/fuzz_finbench.c b/fuzz/fuzz_finbench.c
index 5487dcebf..5e71c6269 100644
--- a/fuzz/fuzz_finbench.c
+++ b/fuzz/fuzz_finbench.c
@@ -26,6 +26,11 @@
* Tests: integer overflow in n_assets*n_assets, weight normalization
* div-by-zero, negative variance sqrt, array bounds
*
+ * - composeCovariance: Sigma = D*R*D composition, correlation validation,
+ * Cholesky positive-definite check
+ * Tests: out-of-range and NaN correlations, asymmetric and ragged
+ * matrices, non-positive pivots, uniform rho below -1/(n-1)
+ *
* - monteCarlo: GBM simulation with exp() overflow guard
* Tests: extreme volatility → exp() overflow, zero volatility,
* negative expected return, 1-simulation edge case, percentile
@@ -118,6 +123,29 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
}
}
+ /*
+ * Exercise 1b: composeCovariance parsing + Sigma = D*R*D + Cholesky.
+ * This hits the correlation range/symmetry validation, the ragged-2D
+ * shape sentinel, and the pivot check (NaN and non-PD inputs).
+ */
+ {
+ finbench_compose_covariance_request_t *req =
+ finbench_compose_covariance_request_create_from_json(env, json_str);
+ if (req) {
+ finbench_compose_covariance_response_t *resp =
+ finbench_compose_covariance(env, req);
+ if (resp) {
+ axis2_char_t *json_out =
+ finbench_compose_covariance_response_to_json(resp, env);
+ if (json_out) {
+ AXIS2_FREE(env->allocator, json_out);
+ }
+ finbench_compose_covariance_response_free(resp, env);
+ }
+ finbench_compose_covariance_request_free(req, env);
+ }
+ }
+
/*
* Exercise 2: Monte Carlo request parsing + GBM simulation.
* This hits the exp() overflow guard, percentile clamping,
@@ -133,7 +161,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
if (req->n_periods > 50) req->n_periods = 50;
finbench_monte_carlo_response_t *resp =
- finbench_calculate_monte_carlo(env, req);
+ finbench_run_monte_carlo(env, req);
if (resp) {
axis2_char_t *json_out =
finbench_monte_carlo_response_to_json(resp, env);
@@ -152,20 +180,20 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
* and the O(n) vs O(1) benchmark paths.
*/
{
- finbench_scenario_analysis_request_t *req =
- finbench_scenario_analysis_request_create_from_json(env, json_str);
+ finbench_scenario_request_t *req =
+ finbench_scenario_request_create_from_json(env, json_str);
if (req) {
- finbench_scenario_analysis_response_t *resp =
- finbench_calculate_scenario_analysis(env, req);
+ finbench_scenario_response_t *resp =
+ finbench_calculate_scenarios(env, req);
if (resp) {
axis2_char_t *json_out =
- finbench_scenario_analysis_response_to_json(resp, env);
+ finbench_scenario_response_to_json(resp, env);
if (json_out) {
AXIS2_FREE(env->allocator, json_out);
}
- finbench_scenario_analysis_response_free(resp, env);
+ finbench_scenario_response_free(resp, env);
}
- finbench_scenario_analysis_request_free(req, env);
+ finbench_scenario_request_free(req, env);
}
}
diff --git a/samples/user_guide/financial-benchmark-service/README.md b/samples/user_guide/financial-benchmark-service/README.md
index 5a9a39cc8..30a91d2ca 100644
--- a/samples/user_guide/financial-benchmark-service/README.md
+++ b/samples/user_guide/financial-benchmark-service/README.md
@@ -99,11 +99,67 @@ Runs Value at Risk (VaR) simulation using Geometric Brownian Motion.
}
```
-### 3. Scenario Analysis (`/scenarioAnalysis`)
+### 3. Compose Covariance (`/composeCovariance`)
+
+Builds the covariance matrix that `portfolioVariance` consumes from per-asset
+volatilities and a correlation structure, and refuses to return one that is not
+positive definite.
+
+**Formula**: Σ_ij = vol_i × vol_j × R_ij (Σ = D·R·D)
+
+`R` is either a full n×n correlation matrix or one uniform correlation `rho`
+applied to every pair. The result is checked with a Cholesky decomposition
+(O(n³/3)); a matrix that fails returns `FAILED` with the failing index and no
+numbers, so nothing non-PSD can reach `portfolioVariance` by this route.
+
+Why it exists: a hypothetical regime ("every vol at 22%, correlation 0.5") has
+no covariance matrix to hand, and vols and correlations are short, bounded and
+checkable where a raw 25-number matrix is not. The result inherits the vols'
+time basis: annualized vols in, annualized Σ out, to be passed to
+`portfolioVariance` with `n_periods_per_year: 1`.
+
+**Example Request** (uniform correlation, the stress case):
+```json
+{
+ "volatilities": [0.243, 0.244, 0.314, 0.219, 0.167],
+ "correlation": 0.8,
+ "asset_ids": ["MSFT", "AAPL", "AMZN", "JPM", "JNJ"]
+}
+```
+
+**Example Response**:
+```json
+{
+ "status": "SUCCESS",
+ "n_assets": 5,
+ "covariance_matrix": [0.059049, 0.047434, 0.061042, 0.042574, 0.032465, ...],
+ "correlation_matrix": [1.0, 0.8, 0.8, 0.8, 0.8, 0.8, 1.0, ...],
+ "volatilities": [0.243, 0.244, 0.314, 0.219, 0.167],
+ "positive_definite": true,
+ "positive_definite_checked": true,
+ "cholesky_failed_at": -1,
+ "min_pivot": 0.00689,
+ "calc_time_us": 3,
+ "asset_ids": ["MSFT", "AAPL", "AMZN", "JPM", "JNJ"]
+}
+```
+
+Feeding that `covariance_matrix` to `portfolioVariance` with weights
+`[0.25, 0.25, 0.20, 0.15, 0.15]` and `n_periods_per_year: 1` gives a portfolio
+volatility of 0.2228; the same five vols with the historical correlation matrix
+give 0.1567.
+
+**Refusals** name the field: a vol that is not > 0, a `rho` outside [−1, 1], an
+asymmetric or non-unit-diagonal `correlation_matrix`, both forms supplied at
+once, or a Cholesky failure (`"Not positive definite: Cholesky failed at index
+2. With 5 assets a uniform correlation must satisfy -0.25 < rho < 1; got -0.5."`).
+`check_positive_definite: false` skips the O(n³) check for very large n.
+
+### 4. Scenario Analysis (`/scenarioAnalysis`)
Demonstrates O(1) hash table lookups vs O(n) linear search — a common optimization in enterprise portfolio systems migrating from array scans to hash-based asset lookups.
-### 4. Service Metadata (`/metadata`)
+### 5. Service Metadata (`/metadata`)
Returns service capabilities, device info, and memory usage.
diff --git a/samples/user_guide/financial-benchmark-service/services.xml b/samples/user_guide/financial-benchmark-service/services.xml
index 77bf75c26..db83cd564 100644
--- a/samples/user_guide/financial-benchmark-service/services.xml
+++ b/samples/user_guide/financial-benchmark-service/services.xml
@@ -41,6 +41,7 @@
Operations:
- portfolioVariance: O(n²) covariance matrix calculation
+ - composeCovariance: Σ = D·R·D from volatilities + correlations, Cholesky-checked
- monteCarlo: Compute-intensive VaR simulation
- scenarioAnalysis: Hash table vs linear lookup comparison
- metadata: Service information and device stats
@@ -135,6 +136,73 @@
<parameter name="responseType">application/json</parameter>
</operation>
+ <!-- ================================================================
+ Operation: composeCovariance
+
+ Build a covariance matrix from per-asset volatilities and a
+ correlation structure (Σ = D·R·D), then check that it is
+ positive definite with a Cholesky decomposition before it can
+ reach portfolioVariance. The validated front door for
+ hypothetical regimes ("every vol 22%, correlation 0.5").
+ ================================================================ -->
+ <operation name="composeCovariance">
+ <parameter name="mcpDescription">Build a covariance matrix from per-asset volatilities and a correlation structure (Σ = D·R·D): either a full correlation matrix or one uniform correlation for every pair. Runs a Cholesky check and refuses a matrix that is not positive definite. Returns the flat row-major covariance_matrix that portfolioVariance accepts.</parameter>
+ <parameter name="mcpReadOnly">true</parameter>
+ <parameter name="mcpIdempotent">true</parameter>
+ <parameter name="mcpInputSchema">{
+ "type": "object",
+ "required": ["volatilities"],
+ "properties": {
+ "n_assets": {"type": "integer", "minimum": 1, "maximum": 3000,
+ "description": "Number of assets. Inferred from volatilities if omitted."},
+ "volatilities": {"type": "array", "items": {"type": "number"},
+ "description": "Per-asset volatilities, each > 0, as decimals (0.22 = 22%). The result inherits their time basis."},
+ "correlation": {"type": "number", "minimum": -1, "maximum": 1,
+ "description": "Uniform off-diagonal correlation applied to every pair. Use this OR correlation_matrix."},
+ "correlation_matrix": {"type": "array", "items": {"type": "number"},
+ "description": "Full n_assets×n_assets correlation matrix, row-major flat or nested rows. Symmetric, unit diagonal, entries in [-1, 1]. Use this OR correlation."},
+ "check_positive_definite": {"type": "boolean", "default": true,
+ "description": "Cholesky check (O(n³)). Refuses a matrix that is not positive definite."},
+ "asset_ids": {"type": "array", "items": {"type": "string"},
+ "description": "Optional labels, one per asset, echoed back."},
+ "request_id": {"type": "string", "description": "Optional correlation ID echoed in the response"}
+ }
+ }</parameter>
+ <description>
+ Compose a covariance matrix: Σ_ij = vol_i × vol_j × R_ij.
+
+ Example JSON Request (uniform correlation):
+ {
+ "volatilities": [0.243, 0.244, 0.314, 0.219, 0.167],
+ "correlation": 0.8,
+ "asset_ids": ["MSFT", "AAPL", "AMZN", "JPM", "JNJ"]
+ }
+
+ Example JSON Response:
+ {
+ "status": "SUCCESS",
+ "n_assets": 5,
+ "covariance_matrix": [0.059049, 0.0474, ...],
+ "correlation_matrix": [1, 0.8, ...],
+ "volatilities": [0.243, ...],
+ "positive_definite": true,
+ "positive_definite_checked": true,
+ "cholesky_failed_at": -1,
+ "min_pivot": 0.00558,
+ "calc_time_us": 3
+ }
+
+ A matrix that fails the Cholesky check returns status FAILED,
+ error_message naming the failing index, and no matrix.
+ </description>
+ <messageReceiver class="axis2_json_rpc_msg_recv"/>
+
+ <parameter name="httpMethod">POST</parameter>
+ <parameter name="httpPath">/composeCovariance</parameter>
+ <parameter name="contentType">application/json</parameter>
+ <parameter name="responseType">application/json</parameter>
+ </operation>
+
<!-- ================================================================
Operation: monteCarlo
@@ -317,7 +385,7 @@
{
"service_name": "FinancialBenchmarkService",
"version": "1.0.0",
- "operations": ["portfolioVariance", "monteCarlo", ...],
+ "operations": ["portfolioVariance", "composeCovariance", "monteCarlo", ...],
"max_assets": 2000,
"max_simulations": 1000000,
"memory_constraint_mb": 512,
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
index c6145ba2e..de7ed36ef 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.c
@@ -36,6 +36,7 @@
#include <stdlib.h>
#include <string.h>
#include <math.h>
+#include <stdarg.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <unistd.h>
@@ -689,6 +690,596 @@ finbench_portfolio_variance_json_only(
return json_response;
}
+/* ============================================================================
+ * Compose Covariance Implementation
+ *
+ * Σ = D·R·D, then Cholesky. See the header for the rationale.
+ * ============================================================================
+ */
+
+/* Set a FAILED status with a formatted message; returns the response for
+ * convenient "return fail(...)" use in the compose function. */
+static finbench_compose_covariance_response_t *
+compose_fail(
+ const axutil_env_t *env,
+ finbench_compose_covariance_response_t *response,
+ const char *fmt, ...)
+{
+ char buf[320];
+ va_list ap;
+
+ va_start(ap, fmt);
+ vsnprintf(buf, sizeof(buf), fmt, ap);
+ va_end(ap);
+
+ if (response->status)
+ AXIS2_FREE(env->allocator, response->status);
+ response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+ response->error_message = axutil_strdup(env, buf);
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI, "FinBench composeCovariance: %s", buf);
+ return response;
+}
+
+AXIS2_EXTERN finbench_compose_covariance_request_t* AXIS2_CALL
+finbench_compose_covariance_request_create_from_json(
+ const axutil_env_t *env,
+ const axis2_char_t *json_string)
+{
+ finbench_compose_covariance_request_t *request = NULL;
+ json_object *json_obj = NULL;
+ json_object *value_obj = NULL;
+ json_object *array_obj = NULL;
+ int i, j;
+
+ if (!env || !json_string) {
+ return NULL;
+ }
+
+ json_obj = json_tokener_parse(json_string);
+ if (!json_obj) {
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "FinBench composeCovariance: Failed to parse JSON request");
+ return NULL;
+ }
+
+ request = AXIS2_MALLOC(env->allocator, sizeof(finbench_compose_covariance_request_t));
+ if (!request) {
+ json_object_put(json_obj);
+ return NULL;
+ }
+ memset(request, 0, sizeof(finbench_compose_covariance_request_t));
+ request->check_positive_definite = AXIS2_TRUE;
+
+ /* n_assets — explicit, or inferred from the volatilities array */
+ if (json_object_object_get_ex(json_obj, "n_assets", &value_obj)) {
+ request->n_assets = json_object_get_int(value_obj);
+ } else if (json_object_object_get_ex(json_obj, "volatilities", &array_obj) &&
+ json_object_is_type(array_obj, json_type_array)) {
+ request->n_assets = json_object_array_length(array_obj);
+ AXIS2_LOG_INFO(env->log,
+ "FinBench composeCovariance: Inferred n_assets=%d from volatilities",
+ request->n_assets);
+ }
+
+ if (request->n_assets <= 0 || request->n_assets > FINBENCH_MAX_ASSETS) {
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+ "FinBench composeCovariance: Invalid n_assets: %d (max: %d)",
+ request->n_assets, FINBENCH_MAX_ASSETS);
+ AXIS2_FREE(env->allocator, request);
+ json_object_put(json_obj);
+ return NULL;
+ }
+
+ /* volatilities */
+ request->volatilities = AXIS2_MALLOC(env->allocator,
+ (size_t)request->n_assets * sizeof(double));
+ if (!request->volatilities) {
+ AXIS2_FREE(env->allocator, request);
+ json_object_put(json_obj);
+ return NULL;
+ }
+ memset(request->volatilities, 0, (size_t)request->n_assets * sizeof(double));
+
+ if (json_object_object_get_ex(json_obj, "volatilities", &array_obj) &&
+ json_object_is_type(array_obj, json_type_array)) {
+ int len = json_object_array_length(array_obj);
+ request->volatilities_provided = len;
+ for (i = 0; i < request->n_assets && i < len; i++) {
+ json_object *elem = json_object_array_get_idx(array_obj, i);
+ request->volatilities[i] = json_object_get_double(elem);
+ }
+ }
+
+ /* correlation (uniform ρ) */
+ if (json_object_object_get_ex(json_obj, "correlation", &value_obj) &&
+ (json_object_is_type(value_obj, json_type_double) ||
+ json_object_is_type(value_obj, json_type_int))) {
+ request->correlation = json_object_get_double(value_obj);
+ request->has_correlation = AXIS2_TRUE;
+ }
+
+ /* correlation_matrix — flat n² or 2D n×n, same shapes as portfolioVariance */
+ if (json_object_object_get_ex(json_obj, "correlation_matrix", &array_obj) &&
+ json_object_is_type(array_obj, json_type_array)) {
+ size_t matrix_size = (size_t)request->n_assets * request->n_assets;
+ int outer_len = json_object_array_length(array_obj);
+ json_object *first_elem = json_object_array_get_idx(array_obj, 0);
+
+ request->correlation_matrix = AXIS2_MALLOC(env->allocator,
+ matrix_size * sizeof(double));
+ if (!request->correlation_matrix) {
+ AXIS2_FREE(env->allocator, request->volatilities);
+ AXIS2_FREE(env->allocator, request);
+ json_object_put(json_obj);
+ return NULL;
+ }
+ memset(request->correlation_matrix, 0, matrix_size * sizeof(double));
+
+ if (first_elem && json_object_is_type(first_elem, json_type_array)) {
+ int total_elements = 0;
+ int shape_ok = (outer_len == request->n_assets);
+ for (i = 0; i < request->n_assets && i < outer_len; i++) {
+ json_object *row = json_object_array_get_idx(array_obj, i);
+ if (row && json_object_is_type(row, json_type_array)) {
+ int row_len = json_object_array_length(row);
+ if (row_len != request->n_assets) shape_ok = 0;
+ total_elements += row_len;
+ for (j = 0; j < request->n_assets && j < row_len; j++) {
+ json_object *cell = json_object_array_get_idx(row, j);
+ request->correlation_matrix[i * request->n_assets + j] =
+ json_object_get_double(cell);
+ }
+ } else {
+ shape_ok = 0;
+ }
+ }
+ request->matrix_elements_provided = shape_ok ? total_elements : -1;
+ } else {
+ request->matrix_elements_provided = outer_len;
+ for (i = 0; i < (int)matrix_size && i < outer_len; i++) {
+ json_object *elem = json_object_array_get_idx(array_obj, i);
+ request->correlation_matrix[i] = json_object_get_double(elem);
+ }
+ }
+ }
+
+ /* check_positive_definite — default true */
+ if (json_object_object_get_ex(json_obj, "check_positive_definite", &value_obj)) {
+ request->check_positive_definite = json_object_get_boolean(value_obj)
+ ? AXIS2_TRUE : AXIS2_FALSE;
+ }
+
+ /* asset_ids (optional; only kept when the length matches) */
+ if (json_object_object_get_ex(json_obj, "asset_ids", &array_obj) &&
+ json_object_is_type(array_obj, json_type_array) &&
+ json_object_array_length(array_obj) == request->n_assets) {
+ request->asset_ids = AXIS2_MALLOC(env->allocator,
+ (size_t)request->n_assets * sizeof(char *));
+ if (request->asset_ids) {
+ memset(request->asset_ids, 0, (size_t)request->n_assets * sizeof(char *));
+ for (i = 0; i < request->n_assets; i++) {
+ json_object *elem = json_object_array_get_idx(array_obj, i);
+ const char *id = elem ? json_object_get_string(elem) : NULL;
+ request->asset_ids[i] = id ? axutil_strdup(env, id) : NULL;
+ }
+ }
+ }
+
+ if (json_object_object_get_ex(json_obj, "request_id", &value_obj)) {
+ const char *rid = json_object_get_string(value_obj);
+ if (rid) {
+ request->request_id = axutil_strdup(env, rid);
+ }
+ }
+
+ json_object_put(json_obj);
+ return request;
+}
+
+AXIS2_EXTERN void AXIS2_CALL
+finbench_compose_covariance_request_free(
+ finbench_compose_covariance_request_t *request,
+ const axutil_env_t *env)
+{
+ if (!request || !env) return;
+
+ if (request->volatilities)
+ AXIS2_FREE(env->allocator, request->volatilities);
+ if (request->correlation_matrix)
+ AXIS2_FREE(env->allocator, request->correlation_matrix);
+ if (request->request_id)
+ AXIS2_FREE(env->allocator, request->request_id);
+ if (request->asset_ids) {
+ int i;
+ for (i = 0; i < request->n_assets; i++) {
+ if (request->asset_ids[i])
+ AXIS2_FREE(env->allocator, request->asset_ids[i]);
+ }
+ AXIS2_FREE(env->allocator, request->asset_ids);
+ }
+
+ AXIS2_FREE(env->allocator, request);
+}
+
+AXIS2_EXTERN finbench_compose_covariance_response_t* AXIS2_CALL
+finbench_compose_covariance_response_create(const axutil_env_t *env)
+{
+ finbench_compose_covariance_response_t *response;
+
+ response = AXIS2_MALLOC(env->allocator,
+ sizeof(finbench_compose_covariance_response_t));
+ if (response) {
+ memset(response, 0, sizeof(finbench_compose_covariance_response_t));
+ response->cholesky_failed_at = -1;
+ }
+ return response;
+}
+
+AXIS2_EXTERN void AXIS2_CALL
+finbench_compose_covariance_response_free(
+ finbench_compose_covariance_response_t *response,
+ const axutil_env_t *env)
+{
+ if (!response || !env) return;
+
+ if (response->status)
+ AXIS2_FREE(env->allocator, response->status);
+ if (response->covariance_matrix)
+ AXIS2_FREE(env->allocator, response->covariance_matrix);
+ if (response->correlation_matrix)
+ AXIS2_FREE(env->allocator, response->correlation_matrix);
+ if (response->volatilities)
+ AXIS2_FREE(env->allocator, response->volatilities);
+ if (response->error_message)
+ AXIS2_FREE(env->allocator, response->error_message);
+ if (response->request_id)
+ AXIS2_FREE(env->allocator, response->request_id);
+ if (response->device_info)
+ AXIS2_FREE(env->allocator, response->device_info);
+ if (response->asset_ids) {
+ int i;
+ for (i = 0; i < response->n_assets; i++) {
+ if (response->asset_ids[i])
+ AXIS2_FREE(env->allocator, response->asset_ids[i]);
+ }
+ AXIS2_FREE(env->allocator, response->asset_ids);
+ }
+
+ AXIS2_FREE(env->allocator, response);
+}
+
+/**
+ * Cholesky–Banachiewicz on a symmetric matrix held row-major in `a`
+ * (n×n). Writes the lower factor L into `l` (row-major, upper part left 0).
+ * Returns -1 on success, else the index of the first pivot that is not
+ * strictly positive — which is exactly "not positive definite".
+ * *min_pivot receives the smallest pivot (L_ii²) seen before any failure.
+ */
+static int
+cholesky_lower(const double *a, double *l, int n, double *min_pivot)
+{
+ int i, j, k;
+ *min_pivot = 0.0;
+
+ for (i = 0; i < n; i++) {
+ for (j = 0; j <= i; j++) {
+ double sum = a[i * n + j];
+ for (k = 0; k < j; k++) {
+ sum -= l[i * n + k] * l[j * n + k];
+ }
+ if (i == j) {
+ /* A pivot that is zero, negative, or NaN means Σ is not
+ * positive definite (or the input carried a NaN). */
+ if (!(sum > 0.0)) {
+ return i;
+ }
+ if (i == 0 || sum < *min_pivot) {
+ *min_pivot = sum;
+ }
+ l[i * n + i] = sqrt(sum);
+ } else {
+ l[i * n + j] = sum / l[j * n + j];
+ }
+ }
+ }
+ return -1;
+}
+
+AXIS2_EXTERN finbench_compose_covariance_response_t* AXIS2_CALL
+finbench_compose_covariance(
+ const axutil_env_t *env,
+ finbench_compose_covariance_request_t *request)
+{
+ finbench_compose_covariance_response_t *response = NULL;
+ long start_time, end_time;
+ int i, j, n;
+ size_t matrix_size;
+
+ response = finbench_compose_covariance_response_create(env);
+ if (!response) return NULL;
+
+ if (!request || !request->volatilities) {
+ return compose_fail(env, response, "Invalid request parameters");
+ }
+
+ n = request->n_assets;
+ response->n_assets = n;
+ matrix_size = (size_t)n * n;
+
+ /* --- Dimension and range validation. Every refusal names the field. --- */
+
+ if (request->volatilities_provided == 0) {
+ return compose_fail(env, response,
+ "Missing required field: \"volatilities\" array (n_assets values, each > 0).");
+ }
+ if (request->volatilities_provided != n) {
+ return compose_fail(env, response,
+ "volatilities array length %d != n_assets %d.",
+ request->volatilities_provided, n);
+ }
+ for (i = 0; i < n; i++) {
+ double v = request->volatilities[i];
+ if (!isfinite(v) || v <= 0.0) {
+ return compose_fail(env, response,
+ "volatilities[%d] = %g must be finite and > 0.", i, v);
+ }
+ }
+
+ if (request->has_correlation && request->correlation_matrix) {
+ return compose_fail(env, response,
+ "Supply either \"correlation\" (uniform) or \"correlation_matrix\", not both.");
+ }
+ if (!request->has_correlation && !request->correlation_matrix) {
+ return compose_fail(env, response,
+ "Missing required field: one of \"correlation\" (uniform rho in [-1, 1]) "
+ "or \"correlation_matrix\" (n_assets x n_assets).");
+ }
+
+ if (request->has_correlation) {
+ double rho = request->correlation;
+ if (!isfinite(rho) || rho < -1.0 || rho > 1.0) {
+ return compose_fail(env, response,
+ "correlation = %g must be within [-1, 1].", rho);
+ }
+ } else {
+ if (request->matrix_elements_provided != (int)matrix_size) {
+ return compose_fail(env, response,
+ "correlation_matrix must have exactly n_assets*n_assets = %d elements "
+ "(flat row-major) or be an n_assets x n_assets 2D array; got %d.",
+ (int)matrix_size, request->matrix_elements_provided);
+ }
+ for (i = 0; i < n; i++) {
+ for (j = 0; j < n; j++) {
+ double r = request->correlation_matrix[i * n + j];
+ if (!isfinite(r) || r < -1.0 || r > 1.0) {
+ return compose_fail(env, response,
+ "correlation_matrix[%d][%d] = %g must be within [-1, 1].", i, j, r);
+ }
+ if (i == j && fabs(r - 1.0) > FINBENCH_CORR_TOL) {
+ return compose_fail(env, response,
+ "correlation_matrix[%d][%d] = %g must be 1 on the diagonal.", i, i, r);
+ }
+ if (j < i && fabs(r - request->correlation_matrix[j * n + i]) > FINBENCH_CORR_TOL) {
+ return compose_fail(env, response,
+ "correlation_matrix is not symmetric at (%d,%d): %g vs %g.",
+ i, j, r, request->correlation_matrix[j * n + i]);
+ }
+ }
+ }
+ }
+
+ /* --- Allocate outputs --- */
+ response->covariance_matrix = AXIS2_MALLOC(env->allocator, matrix_size * sizeof(double));
+ response->correlation_matrix = AXIS2_MALLOC(env->allocator, matrix_size * sizeof(double));
+ response->volatilities = AXIS2_MALLOC(env->allocator, (size_t)n * sizeof(double));
+ if (!response->covariance_matrix || !response->correlation_matrix || !response->volatilities) {
+ return compose_fail(env, response, "Memory allocation failed for %d assets.", n);
+ }
+
+ start_time = get_time_us();
+
+ /* --- Σ = D·R·D --- */
+ for (i = 0; i < n; i++) {
+ response->volatilities[i] = request->volatilities[i];
+ }
+ for (i = 0; i < n; i++) {
+ for (j = 0; j < n; j++) {
+ double r;
+ if (request->has_correlation) {
+ r = (i == j) ? 1.0 : request->correlation;
+ } else {
+ r = request->correlation_matrix[i * n + j];
+ }
+ response->correlation_matrix[i * n + j] = r;
+ response->covariance_matrix[i * n + j] =
+ request->volatilities[i] * request->volatilities[j] * r;
+ }
+ }
+
+ /* --- Cholesky --- */
+ if (request->check_positive_definite) {
+ double *l = AXIS2_MALLOC(env->allocator, matrix_size * sizeof(double));
+ int failed_at;
+ double min_pivot = 0.0;
+
+ if (!l) {
+ return compose_fail(env, response,
+ "Memory allocation failed for the Cholesky factor (%d assets).", n);
+ }
+ memset(l, 0, matrix_size * sizeof(double));
+ failed_at = cholesky_lower(response->covariance_matrix, l, n, &min_pivot);
+ AXIS2_FREE(env->allocator, l);
+
+ response->positive_definite_checked = AXIS2_TRUE;
+ response->min_pivot = min_pivot;
+ response->cholesky_failed_at = failed_at;
+ response->positive_definite = (failed_at < 0) ? AXIS2_TRUE : AXIS2_FALSE;
+
+ if (failed_at >= 0) {
+ /* Do not hand back a matrix that portfolioVariance would silently
+ * accept. The caller gets the reason and the index, not numbers. */
+ AXIS2_FREE(env->allocator, response->covariance_matrix);
+ AXIS2_FREE(env->allocator, response->correlation_matrix);
+ AXIS2_FREE(env->allocator, response->volatilities);
+ response->covariance_matrix = NULL;
+ response->correlation_matrix = NULL;
+ response->volatilities = NULL;
+ if (request->has_correlation) {
+ return compose_fail(env, response,
+ "Not positive definite: Cholesky failed at index %d. "
+ "With %d assets a uniform correlation must satisfy %g < rho < 1; got %g.",
+ failed_at, n, (n > 1) ? -1.0 / (double)(n - 1) : -1.0, request->correlation);
+ }
+ return compose_fail(env, response,
+ "Not positive definite: Cholesky failed at index %d "
+ "(the leading %dx%d block of the correlation matrix is not positive definite).",
+ failed_at, failed_at + 1, failed_at + 1);
+ }
+ }
+
+ end_time = get_time_us();
+
+ response->status = axutil_strdup(env, FINBENCH_STATUS_SUCCESS);
+ response->calc_time_us = end_time - start_time;
+ response->memory_used_kb = finbench_get_memory_usage_kb();
+
+ if (request->asset_ids) {
+ response->asset_ids = AXIS2_MALLOC(env->allocator, (size_t)n * sizeof(char *));
+ if (response->asset_ids) {
+ for (i = 0; i < n; i++) {
+ response->asset_ids[i] = request->asset_ids[i]
+ ? axutil_strdup(env, request->asset_ids[i]) : NULL;
+ }
+ }
+ }
+ if (request->request_id) {
+ response->request_id = axutil_strdup(env, request->request_id);
+ }
+ response->device_info = finbench_get_device_info(env);
+
+ AXIS2_LOG_INFO(env->log,
+ "FinBench: composeCovariance for %d assets in %ld us (positive_definite=%s)",
+ n, response->calc_time_us,
+ response->positive_definite_checked
+ ? (response->positive_definite ? "true" : "false") : "unchecked");
+
+ return response;
+}
+
+AXIS2_EXTERN axis2_char_t* AXIS2_CALL
+finbench_compose_covariance_response_to_json(
+ const finbench_compose_covariance_response_t *response,
+ const axutil_env_t *env)
+{
+ json_object *json_resp;
+ const char *json_str;
+ axis2_char_t *result;
+ int i;
+ size_t matrix_size;
+
+ if (!response || !env) return NULL;
+
+ matrix_size = (size_t)response->n_assets * response->n_assets;
+ json_resp = json_object_new_object();
+
+ json_object_object_add(json_resp, "status",
+ json_object_new_string(response->status ? response->status : "UNKNOWN"));
+
+ json_object_object_add(json_resp, "n_assets",
+ json_object_new_int(response->n_assets));
+
+ if (response->covariance_matrix) {
+ json_object *arr = json_object_new_array();
+ for (i = 0; i < (int)matrix_size; i++)
+ json_object_array_add(arr, json_object_new_double(response->covariance_matrix[i]));
+ json_object_object_add(json_resp, "covariance_matrix", arr);
+ }
+ if (response->correlation_matrix) {
+ json_object *arr = json_object_new_array();
+ for (i = 0; i < (int)matrix_size; i++)
+ json_object_array_add(arr, json_object_new_double(response->correlation_matrix[i]));
+ json_object_object_add(json_resp, "correlation_matrix", arr);
+ }
+ if (response->volatilities) {
+ json_object *arr = json_object_new_array();
+ for (i = 0; i < response->n_assets; i++)
+ json_object_array_add(arr, json_object_new_double(response->volatilities[i]));
+ json_object_object_add(json_resp, "volatilities", arr);
+ }
+
+ json_object_object_add(json_resp, "positive_definite",
+ json_object_new_boolean(response->positive_definite));
+ json_object_object_add(json_resp, "positive_definite_checked",
+ json_object_new_boolean(response->positive_definite_checked));
+ json_object_object_add(json_resp, "cholesky_failed_at",
+ json_object_new_int(response->cholesky_failed_at));
+ json_object_object_add(json_resp, "min_pivot",
+ json_object_new_double(response->min_pivot));
+
+ json_object_object_add(json_resp, "calc_time_us",
+ json_object_new_int64(response->calc_time_us));
+ json_object_object_add(json_resp, "memory_used_kb",
+ json_object_new_int(response->memory_used_kb));
+
+ if (response->asset_ids) {
+ json_object *arr = json_object_new_array();
+ for (i = 0; i < response->n_assets; i++)
+ json_object_array_add(arr, response->asset_ids[i]
+ ? json_object_new_string(response->asset_ids[i]) : NULL);
+ json_object_object_add(json_resp, "asset_ids", arr);
+ }
+ if (response->request_id) {
+ json_object_object_add(json_resp, "request_id",
+ json_object_new_string(response->request_id));
+ }
+ if (response->device_info) {
+ json_object_object_add(json_resp, "device_info",
+ json_object_new_string(response->device_info));
+ }
+ if (response->error_message) {
+ json_object_object_add(json_resp, "error_message",
+ json_object_new_string(response->error_message));
+ }
+
+ json_str = json_object_to_json_string_ext(json_resp, JSON_C_TO_STRING_PLAIN);
+ result = axutil_strdup(env, json_str);
+ json_object_put(json_resp);
+
+ return result;
+}
+
+/**
+ * HTTP/2 JSON endpoint for compose covariance
+ */
+AXIS2_EXTERN axis2_char_t* AXIS2_CALL
+finbench_compose_covariance_json_only(
+ const axutil_env_t *env,
+ const axis2_char_t *json_request)
+{
+ finbench_compose_covariance_request_t *request;
+ finbench_compose_covariance_response_t *response;
+ axis2_char_t *json_response;
+
+ request = finbench_compose_covariance_request_create_from_json(env, json_request);
+ if (!request) {
+ return axutil_strdup(env,
+ "{\"status\":\"FAILED\",\"error_message\":"
+ "\"Failed to parse composeCovariance request. "
+ "Required fields: volatilities (float[] > 0) and one of "
+ "correlation (float in [-1,1]) or correlation_matrix (float[n²] flat or float[n][n] 2D). "
+ "Optional: n_assets (int), check_positive_definite (bool, default true), "
+ "asset_ids (string[]), request_id (string).\"}");
+ }
+
+ response = finbench_compose_covariance(env, request);
+ json_response = finbench_compose_covariance_response_to_json(response, env);
+
+ finbench_compose_covariance_request_free(request, env);
+ finbench_compose_covariance_response_free(response, env);
+
+ return json_response;
+}
+
/* ============================================================================
* Monte Carlo Simulation Implementation
* ============================================================================
@@ -2050,6 +2641,7 @@ finbench_get_metadata_json(const axutil_env_t *env)
/* Operations */
ops_array = json_object_new_array();
json_object_array_add(ops_array, json_object_new_string("portfolioVariance"));
+ json_object_array_add(ops_array, json_object_new_string("composeCovariance"));
json_object_array_add(ops_array, json_object_new_string("monteCarlo"));
json_object_array_add(ops_array, json_object_new_string("scenarioAnalysis"));
json_object_array_add(ops_array, json_object_new_string("metadata"));
@@ -2217,6 +2809,11 @@ finbench_dispatch_json_obj(
{
result_str = finbench_portfolio_variance_json_only(env, json_str);
}
+ else if (strcmp(action, "composeCovariance") == 0 ||
+ strcmp(action, "compose_covariance") == 0)
+ {
+ result_str = finbench_compose_covariance_json_only(env, json_str);
+ }
else if (strcmp(action, "monteCarlo") == 0 ||
strcmp(action, "monte_carlo") == 0)
{
@@ -2265,6 +2862,13 @@ finbench_dispatch_json_obj(
"FinancialBenchmarkService: Detected portfolioVariance request");
result_str = finbench_portfolio_variance_json_only(env, json_str);
}
+ /* If request has 'volatilities', it's composeCovariance */
+ else if (json_object_object_get_ex(json_request, "volatilities", &temp_obj))
+ {
+ AXIS2_LOG_INFO(env->log,
+ "FinancialBenchmarkService: Detected composeCovariance request");
+ result_str = finbench_compose_covariance_json_only(env, json_str);
+ }
/* If request has 'n_simulations', it's Monte Carlo */
else if (json_object_object_get_ex(json_request, "n_simulations", &temp_obj))
{
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.h b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.h
index 2a482365f..971785902 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.h
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service.h
@@ -27,6 +27,7 @@
*
* Features:
* - Portfolio variance calculation (O(n²) matrix operations)
+ * - Covariance composition Σ = D·R·D with a Cholesky positive-definite check
* - Monte Carlo simulation (compute-intensive)
* - Scenario analysis with hash table lookups
* - Memory-efficient operation for 1-2GB devices
@@ -223,6 +224,146 @@ typedef struct finbench_portfolio_variance_response
} finbench_portfolio_variance_response_t;
+/* ============================================================================
+ * Compose Covariance
+ *
+ * Builds a covariance matrix from per-asset volatilities and a correlation
+ * structure, and checks that the result is positive definite before anyone
+ * feeds it to portfolioVariance. This is the Σ = D·R·D decomposition:
+ *
+ * Σ_ij = vol_i · vol_j · R_ij
+ *
+ * where D = diag(vol) and R is the correlation matrix. R is either supplied
+ * in full (n×n, flat row-major or 2D) or as a single uniform off-diagonal
+ * value ρ (R_ii = 1, R_ij = ρ for i≠j) — the "all correlations go to 0.8"
+ * stress case.
+ *
+ * Why this exists:
+ * A caller who wants a hypothetical regime — "every vol at 22%, correlation
+ * 0.5" — has no covariance matrix to hand and should not type one. Vols and
+ * correlations are short, human-readable, and bounded ([−1, 1] for ρ,
+ * > 0 for vol), so they can be validated. A 5×5 covariance matrix is 25
+ * unlabeled numbers that cannot. This operation is the validated front
+ * door; portfolioVariance stays the raw one.
+ *
+ * Positive definiteness:
+ * portfolioVariance documents that it does not check PSD and that a bad
+ * matrix yields a clamped-to-zero variance with no error. This operation
+ * closes that gap for matrices it produces: it runs a Cholesky
+ * decomposition (Σ = L·Lᵀ) and reports FAILED with the failing pivot
+ * index if any pivot is ≤ 0. Cholesky is O(n³/3), so on constrained
+ * hardware a 3000-asset check costs seconds; check_positive_definite=false
+ * skips it. For the uniform-ρ form the condition is closed-form:
+ * Σ is positive definite iff −1/(n−1) < ρ < 1 (with all vols > 0), and the
+ * Cholesky result agrees with that.
+ *
+ * Time basis:
+ * Σ inherits the basis of the vols. Annualized vols in → annualized Σ out,
+ * which is the usual case; pass that Σ to portfolioVariance with
+ * n_periods_per_year=1. The service does not annualize or de-annualize.
+ * ============================================================================
+ */
+
+/** Tolerance for correlation-matrix symmetry and unit-diagonal checks. */
+#define FINBENCH_CORR_TOL 1e-6
+
+/**
+ * @brief Compose Covariance Request
+ */
+typedef struct finbench_compose_covariance_request
+{
+ /** Number of assets (must be in [1, FINBENCH_MAX_ASSETS]); inferred from volatilities if absent. */
+ int n_assets;
+
+ /** Per-asset volatilities (exactly n_assets, each finite and > 0). Basis is the caller's. */
+ double *volatilities;
+
+ /** Length of volatilities[] as parsed, for dimension validation. */
+ int volatilities_provided;
+
+ /**
+ * Uniform off-diagonal correlation ρ, used when correlation_matrix is
+ * absent. Must be in [−1, 1]. has_correlation records whether the field
+ * was present at all.
+ */
+ double correlation;
+ axis2_bool_t has_correlation;
+
+ /**
+ * Full correlation matrix, flattened row-major (element (i,j) at
+ * i*n_assets + j), or NULL when the uniform form is used. Accepts the
+ * same flat or 2D JSON shapes as portfolioVariance's covariance_matrix.
+ * Must be symmetric within FINBENCH_CORR_TOL, have a unit diagonal
+ * within FINBENCH_CORR_TOL, and every entry in [−1, 1].
+ */
+ double *correlation_matrix;
+
+ /** Flat element count of correlation_matrix as parsed; -1 for a ragged 2D shape. */
+ int matrix_elements_provided;
+
+ /** Run the Cholesky positive-definiteness check (default true). */
+ axis2_bool_t check_positive_definite;
+
+ /** Optional asset identifiers, echoed in the response (exactly n_assets if present). */
+ char **asset_ids;
+
+ /** Request identifier for tracing */
+ char *request_id;
+
+} finbench_compose_covariance_request_t;
+
+/**
+ * @brief Compose Covariance Response
+ */
+typedef struct finbench_compose_covariance_response
+{
+ /** Processing status */
+ char *status;
+
+ /** Number of assets */
+ int n_assets;
+
+ /** Σ, flattened row-major, n_assets² elements. NULL on FAILED. */
+ double *covariance_matrix;
+
+ /** R as used (uniform ρ expanded, or the caller's), flattened row-major. NULL on FAILED. */
+ double *correlation_matrix;
+
+ /** Volatilities as supplied. NULL on FAILED. */
+ double *volatilities;
+
+ /** Whether Cholesky succeeded. AXIS2_FALSE also when the check was skipped. */
+ axis2_bool_t positive_definite;
+
+ /** Whether the Cholesky check ran at all (check_positive_definite). */
+ axis2_bool_t positive_definite_checked;
+
+ /** Index of the first non-positive Cholesky pivot, or -1. */
+ int cholesky_failed_at;
+
+ /** Smallest Cholesky pivot L_ii² seen (a conditioning diagnostic); 0 if not checked. */
+ double min_pivot;
+
+ /** Processing time in microseconds */
+ long calc_time_us;
+
+ /** Peak memory used in KB */
+ int memory_used_kb;
+
+ /** Error message (if status == FAILED) */
+ char *error_message;
+
+ /** Asset identifiers echo (may be NULL) */
+ char **asset_ids;
+
+ /** Request ID echo */
+ char *request_id;
+
+ /** Device info for demo purposes */
+ char *device_info;
+
+} finbench_compose_covariance_response_t;
+
/* ============================================================================
* Monte Carlo Simulation
*
@@ -773,6 +914,66 @@ finbench_portfolio_variance_json_only(
const axutil_env_t *env,
const axis2_char_t *json_request);
+/* ============================================================================
+ * Function Declarations - Compose Covariance
+ * ============================================================================
+ */
+
+/**
+ * @brief Create compose-covariance request from JSON
+ */
+AXIS2_EXTERN finbench_compose_covariance_request_t* AXIS2_CALL
+finbench_compose_covariance_request_create_from_json(
+ const axutil_env_t *env,
+ const axis2_char_t *json_string);
+
+/**
+ * @brief Free compose-covariance request
+ */
+AXIS2_EXTERN void AXIS2_CALL
+finbench_compose_covariance_request_free(
+ finbench_compose_covariance_request_t *request,
+ const axutil_env_t *env);
+
+/**
+ * @brief Create compose-covariance response
+ */
+AXIS2_EXTERN finbench_compose_covariance_response_t* AXIS2_CALL
+finbench_compose_covariance_response_create(
+ const axutil_env_t *env);
+
+/**
+ * @brief Convert compose-covariance response to JSON
+ */
+AXIS2_EXTERN axis2_char_t* AXIS2_CALL
+finbench_compose_covariance_response_to_json(
+ const finbench_compose_covariance_response_t *response,
+ const axutil_env_t *env);
+
+/**
+ * @brief Free compose-covariance response
+ */
+AXIS2_EXTERN void AXIS2_CALL
+finbench_compose_covariance_response_free(
+ finbench_compose_covariance_response_t *response,
+ const axutil_env_t *env);
+
+/**
+ * @brief Compose Σ = D·R·D and check positive definiteness (main operation)
+ */
+AXIS2_EXTERN finbench_compose_covariance_response_t* AXIS2_CALL
+finbench_compose_covariance(
+ const axutil_env_t *env,
+ finbench_compose_covariance_request_t *request);
+
+/**
+ * @brief Process compose-covariance with pure JSON (HTTP/2 endpoint)
+ */
+AXIS2_EXTERN axis2_char_t* AXIS2_CALL
+finbench_compose_covariance_json_only(
+ const axutil_env_t *env,
+ const axis2_char_t *json_request);
+
/* ============================================================================
* Function Declarations - Monte Carlo
* ============================================================================
diff --git a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
index f19285ef4..6576908b1 100644
--- a/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
+++ b/samples/user_guide/financial-benchmark-service/src/financial_benchmark_service_handler.c
@@ -463,6 +463,12 @@ route_operation(
result = finbench_portfolio_variance_json_only(env, json_request);
}
+ /* composeCovariance operation */
+ else if (axutil_strcmp(operation_name, "composeCovariance") == 0)
+ {
+ result = finbench_compose_covariance_json_only(env, json_request);
+ }
+
/* monteCarlo operation */
else if (axutil_strcmp(operation_name, "monteCarlo") == 0)
{
diff --git a/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c b/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
index b51e99015..30a880588 100644
--- a/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
+++ b/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
@@ -84,6 +84,34 @@ static const char SCHEMA_PORTFOLIO_VARIANCE[] =
"\"required\":[\"n_assets\",\"weights\",\"covariance_matrix\"]"
"}";
+static const char SCHEMA_COMPOSE_COVARIANCE[] =
+ "{"
+ "\"type\":\"object\","
+ "\"properties\":{"
+ "\"n_assets\":{\"type\":\"integer\","
+ "\"description\":\"Number of assets (max 3000). Inferred from volatilities if omitted\"},"
+ "\"volatilities\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},"
+ "\"description\":\"Per-asset volatilities, each > 0, as decimals (0.22 = 22%%). "
+ "The covariance matrix inherits their time basis: annualized vols give an "
+ "annualized matrix, to be passed to portfolioVariance with n_periods_per_year=1\"},"
+ "\"correlation\":{\"type\":\"number\",\"minimum\":-1,\"maximum\":1,"
+ "\"description\":\"Uniform off-diagonal correlation rho applied to every pair "
+ "(the 'all correlations go to 0.8' stress case). Use this OR correlation_matrix\"},"
+ "\"correlation_matrix\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},"
+ "\"description\":\"Full n_assets x n_assets correlation matrix, flattened row-major "
+ "(or nested rows). Must be symmetric with a unit diagonal and entries in [-1, 1]. "
+ "Use this OR correlation\"},"
+ "\"check_positive_definite\":{\"type\":\"boolean\","
+ "\"description\":\"Run a Cholesky decomposition and refuse a matrix that is not "
+ "positive definite. Default true. O(n^3); set false only for very large n\"},"
+ "\"asset_ids\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},"
+ "\"description\":\"Optional labels, one per asset, echoed in the response\"},"
+ "\"request_id\":{\"type\":\"string\","
+ "\"description\":\"Optional identifier echoed in the response for request tracing\"}"
+ "},"
+ "\"required\":[\"volatilities\"]"
+ "}";
+
static const char SCHEMA_MONTE_CARLO[] =
"{"
"\"type\":\"object\","
@@ -174,6 +202,17 @@ static const finbench_mcp_tool_t finbench_mcp_tools[] = {
"Target: 500 assets in ~5ms on resource-constrained hardware.",
SCHEMA_PORTFOLIO_VARIANCE
},
+ {
+ "composeCovariance",
+ "Build a covariance matrix from per-asset volatilities and a correlation structure: "
+ "Sigma_ij = vol_i * vol_j * R_ij (Sigma = D*R*D). R is either a full correlation matrix "
+ "or one uniform rho for every pair. Runs a Cholesky decomposition and refuses the "
+ "result if it is not positive definite, naming the failing index. Output is the flat "
+ "row-major covariance_matrix that portfolioVariance accepts, plus the correlation "
+ "matrix used and the vols echoed. Use this instead of typing covariances: vols and "
+ "correlations are short, bounded and checkable; a raw matrix is not.",
+ SCHEMA_COMPOSE_COVARIANCE
+ },
{
"monteCarlo",
"Monte Carlo Value at Risk (VaR) simulation using Geometric Brownian Motion: "
@@ -373,6 +412,8 @@ static json_object *mcp_handle_tools_call(
if (strcmp(tool_name, "portfolioVariance") == 0) {
result_json = finbench_portfolio_variance_json_only(env, args_json);
+ } else if (strcmp(tool_name, "composeCovariance") == 0) {
+ result_json = finbench_compose_covariance_json_only(env, args_json);
} else if (strcmp(tool_name, "monteCarlo") == 0) {
result_json = finbench_monte_carlo_json_only(env, args_json);
} else if (strcmp(tool_name, "scenarioAnalysis") == 0) {
@@ -380,7 +421,7 @@ static json_object *mcp_handle_tools_call(
} else {
AXIS2_FREE(env->allocator, args_json);
*out_code = MCP_ERR_METHOD_NOT_FOUND;
- *out_msg = "Unknown tool name. Available: portfolioVariance, monteCarlo, scenarioAnalysis";
+ *out_msg = "Unknown tool name. Available: portfolioVariance, composeCovariance, monteCarlo, scenarioAnalysis";
return NULL;
}
diff --git a/samples/user_guide/financial-benchmark-service/src/finbench_mcp.h b/samples/user_guide/financial-benchmark-service/src/finbench_mcp.h
index 455becfbe..f7af5ae32 100644
--- a/samples/user_guide/financial-benchmark-service/src/finbench_mcp.h
+++ b/samples/user_guide/financial-benchmark-service/src/finbench_mcp.h
@@ -22,8 +22,8 @@
* @file finbench_mcp.h
* @brief MCP stdio transport for the Financial Benchmark Service (C1)
*
- * Implements JSON-RPC 2.0 over stdin/stdout for the three financial benchmark
- * operations: portfolioVariance, monteCarlo, scenarioAnalysis.
+ * Implements JSON-RPC 2.0 over stdin/stdout for the four financial benchmark
+ * operations: portfolioVariance, composeCovariance, monteCarlo, scenarioAnalysis.
*
* This is a thin wrapper around the existing finbench_*_json_only() functions.
* The same JSON request/response schemas are reused — MCP adds only the
@@ -50,6 +50,7 @@
*
* Tools exposed:
* - portfolioVariance (O(n^2) covariance matrix multiplication)
+ * - composeCovariance (Sigma = D*R*D from vols + correlations, Cholesky-checked)
* - monteCarlo (GBM Value-at-Risk simulation)
* - scenarioAnalysis (expected return + HashMap vs ArrayList benchmark)
*/