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/23 00:06:25 UTC

(axis-axis2-c-core) branch master updated (5e4ac649c -> 373f5cea9)

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

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


    from 5e4ac649c Fail closed and escape correctly in the operation trust guard
     new 0446624d3 Add covarianceFromReturns to the financial benchmark sample
     new eca314fb6 Let an embedding application add MCP tools to the sample transport
     new d5864fd66 Default portfolioVariance to an annualized matrix and echo the basis
     new 22274a421 Refuse asymmetric and non-PSD matrices in portfolioVariance
     new 410f52516 Add the financial-benchmark fuzzer to the OSS-Fuzz build with seeds
     new 1393ea33c Check the diagonal for finiteness in portfolioVariance
     new 373f5cea9 Refuse an annualized matrix labelled with n_periods_per_year > 1

The 7 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 SECURITY.md                                        |   3 +-
 docs/HTTP2_ANDROID.md                              |  11 +-
 docs/MCP_EXAMPLES.md                               |  76 +-
 docs/OSS-FUZZ.md                                   |  12 +-
 fuzz/corpus/finbench/compose_bad_vol.json          |   1 +
 fuzz/corpus/finbench/compose_matrix.json           |   1 +
 fuzz/corpus/finbench/compose_not_pd.json           |   1 +
 fuzz/corpus/finbench/compose_uniform.json          |   1 +
 fuzz/corpus/finbench/covret_constant.json          |   1 +
 fuzz/corpus/finbench/covret_flat.json              |   1 +
 fuzz/corpus/finbench/covret_nested.json            |   1 +
 fuzz/corpus/finbench/covret_nulls.json             |   1 +
 fuzz/corpus/finbench/mc_correlated_merton.json     |   1 +
 fuzz/corpus/finbench/mc_correlated_not_pd.json     |   1 +
 fuzz/corpus/finbench/mc_empty.json                 |   1 +
 fuzz/corpus/finbench/mc_negative_weight.json       |   1 +
 fuzz/corpus/finbench/mc_scalar.json                |   1 +
 fuzz/corpus/finbench/pv_april_annual.json          |   8 +
 fuzz/corpus/finbench/pv_asymmetric.json            |   1 +
 fuzz/corpus/finbench/pv_daily_basis.json           |   1 +
 fuzz/corpus/finbench/pv_five_asset_equal.json      |   1 +
 fuzz/corpus/finbench/pv_nested_shape.json          |   1 +
 fuzz/corpus/finbench/pv_not_psd.json               |   1 +
 fuzz/finbench.dict                                 |  31 +
 fuzz/fuzz_finbench.c                               |  34 +
 fuzz/oss-fuzz/build.sh                             |  29 +
 .../financial-benchmark-service/README.md          | 119 ++-
 .../financial-benchmark-service/services.xml       |  82 +-
 .../src/financial_benchmark_service.c              | 896 ++++++++++++++++++++-
 .../src/financial_benchmark_service.h              | 267 +++++-
 .../src/financial_benchmark_service_handler.c      |   6 +
 .../financial-benchmark-service/src/finbench_mcp.c | 139 +++-
 .../financial-benchmark-service/src/finbench_mcp.h |  57 +-
 33 files changed, 1697 insertions(+), 91 deletions(-)
 create mode 100644 fuzz/corpus/finbench/compose_bad_vol.json
 create mode 100644 fuzz/corpus/finbench/compose_matrix.json
 create mode 100644 fuzz/corpus/finbench/compose_not_pd.json
 create mode 100644 fuzz/corpus/finbench/compose_uniform.json
 create mode 100644 fuzz/corpus/finbench/covret_constant.json
 create mode 100644 fuzz/corpus/finbench/covret_flat.json
 create mode 100644 fuzz/corpus/finbench/covret_nested.json
 create mode 100644 fuzz/corpus/finbench/covret_nulls.json
 create mode 100644 fuzz/corpus/finbench/mc_correlated_merton.json
 create mode 100644 fuzz/corpus/finbench/mc_correlated_not_pd.json
 create mode 100644 fuzz/corpus/finbench/mc_empty.json
 create mode 100644 fuzz/corpus/finbench/mc_negative_weight.json
 create mode 100644 fuzz/corpus/finbench/mc_scalar.json
 create mode 100644 fuzz/corpus/finbench/pv_april_annual.json
 create mode 100644 fuzz/corpus/finbench/pv_asymmetric.json
 create mode 100644 fuzz/corpus/finbench/pv_daily_basis.json
 create mode 100644 fuzz/corpus/finbench/pv_five_asset_equal.json
 create mode 100644 fuzz/corpus/finbench/pv_nested_shape.json
 create mode 100644 fuzz/corpus/finbench/pv_not_psd.json
 create mode 100644 fuzz/finbench.dict


(axis-axis2-c-core) 05/07: Add the financial-benchmark fuzzer to the OSS-Fuzz build with seeds

Posted by ro...@apache.org.
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 410f5251680f1bcf29ae87a833821ea40b2f3e34
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Sun Sep 20 20:28:48 2026 -1000

    Add the financial-benchmark fuzzer to the OSS-Fuzz build with seeds
    
    fuzz_finbench existed but was never built by fuzz/oss-fuzz/build.sh, so
    the service's request-to-response path ran only when someone fuzzed it
    by hand. It is now a target, compiled with the service source and linked
    like the JSON reader fuzzer, with a 19-file seed corpus covering a valid
    and a deliberately invalid request per operation and a key dictionary.
    Local libFuzzer + ASan run: 615K inputs in 90 s, 729 edges, no crashes.
    
    Co-Authored-By: Claude Fable 5.1 <no...@anthropic.com>
---
 SECURITY.md                                    |  3 +--
 docs/OSS-FUZZ.md                               | 12 ++++++++--
 fuzz/corpus/finbench/compose_bad_vol.json      |  1 +
 fuzz/corpus/finbench/compose_matrix.json       |  1 +
 fuzz/corpus/finbench/compose_not_pd.json       |  1 +
 fuzz/corpus/finbench/compose_uniform.json      |  1 +
 fuzz/corpus/finbench/covret_constant.json      |  1 +
 fuzz/corpus/finbench/covret_flat.json          |  1 +
 fuzz/corpus/finbench/covret_nested.json        |  1 +
 fuzz/corpus/finbench/covret_nulls.json         |  1 +
 fuzz/corpus/finbench/mc_correlated_merton.json |  1 +
 fuzz/corpus/finbench/mc_correlated_not_pd.json |  1 +
 fuzz/corpus/finbench/mc_empty.json             |  1 +
 fuzz/corpus/finbench/mc_negative_weight.json   |  1 +
 fuzz/corpus/finbench/mc_scalar.json            |  1 +
 fuzz/corpus/finbench/pv_april_annual.json      |  8 +++++++
 fuzz/corpus/finbench/pv_asymmetric.json        |  1 +
 fuzz/corpus/finbench/pv_daily_basis.json       |  1 +
 fuzz/corpus/finbench/pv_five_asset_equal.json  |  1 +
 fuzz/corpus/finbench/pv_nested_shape.json      |  1 +
 fuzz/corpus/finbench/pv_not_psd.json           |  1 +
 fuzz/finbench.dict                             | 31 ++++++++++++++++++++++++++
 fuzz/oss-fuzz/build.sh                         | 29 ++++++++++++++++++++++++
 23 files changed, 97 insertions(+), 4 deletions(-)

diff --git a/SECURITY.md b/SECURITY.md
index 1ac622709..19478e682 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -231,8 +231,7 @@ Axis2/C has an active OSS-Fuzz integration with 5 fuzz targets:
 | `fuzz_http_header` | HTTP header parsing |
 | `fuzz_url_parser` | URL parsing and SSRF detection |
 
-Additionally, `fuzz_finbench` tests the FinancialBenchmarkService at
-the application level with integer overflow and boundary value inputs.
+| `fuzz_finbench` | FinancialBenchmarkService sample: `portfolioVariance`, `composeCovariance`, `covarianceFromReturns`, `monteCarlo` request → compute → response, with its own seed corpus and key dictionary |
 
 Local fuzzing validated with 8M+ iterations and zero ASAN errors.
 See [docs/OSS-FUZZ.md](docs/OSS-FUZZ.md) for details.
diff --git a/docs/OSS-FUZZ.md b/docs/OSS-FUZZ.md
index 368e4a589..6a3232dbe 100644
--- a/docs/OSS-FUZZ.md
+++ b/docs/OSS-FUZZ.md
@@ -29,7 +29,7 @@ Traditional testing checks expected inputs. Fuzzing finds bugs with *unexpected*
 
 ### Overview
 
-The `fuzz/` directory contains 5 libFuzzer-compatible targets:
+The `fuzz/` directory contains 6 libFuzzer-compatible targets:
 
 | Target | Component | Protocol Path | Attack Vectors |
 |--------|-----------|---------------|----------------|
@@ -38,6 +38,13 @@ The `fuzz/` directory contains 5 libFuzzer-compatible targets:
 | `fuzz_json_reader.c` | axis2_json_reader | HTTP/1.1 JSON | JSON→AXIOM conversion bugs |
 | `fuzz_http_header.c` | HTTP header parsing | HTTP transport | Header injection, overflows |
 | `fuzz_url_parser.c` | URL parsing (axutil_url) | Client URLs | SSRF, malformed URL handling |
+| `fuzz_finbench.c` | FinancialBenchmarkService sample (`portfolioVariance`, `composeCovariance`, `covarianceFromReturns`, `monteCarlo`) | JSON request → compute → JSON response | Integer overflow in `n × n` allocations, NaN/Inf reaching indexing, ragged or oversized arrays, Cholesky on non-PD input, exp() overflow in the simulation |
+
+`fuzz_finbench` ships its own seed corpus (`fuzz/corpus/finbench/`, one valid
+or deliberately invalid request per operation so every refusal branch is
+reached from the start) and a dictionary of the request keys
+(`fuzz/finbench.dict`); the build script packages both. It runs slower than the
+parser targets because each input executes a full compute path.
 
 ### JSON Fuzzer Architecture
 
@@ -421,7 +428,7 @@ Ensure `autoreconf -i` runs before `./configure` in the build script.
 
 ### Validated Test Results
 
-All 5 fuzz targets have been validated with libFuzzer + AddressSanitizer:
+All 6 fuzz targets have been validated with libFuzzer + AddressSanitizer:
 
 | Target | Runs | Exec/sec | Result |
 |--------|------|----------|--------|
@@ -430,6 +437,7 @@ All 5 fuzz targets have been validated with libFuzzer + AddressSanitizer:
 | fuzz_xml_parser | 4M+ | ~120K | No crashes |
 | fuzz_http_header | 4M+ | ~125K | No crashes |
 | fuzz_url_parser | 4M+ | ~115K | No crashes |
+| fuzz_finbench | 615K (90 s, seeded, with dictionary) | ~6.7K | No crashes; 729 edges, 1,081 corpus units added |
 
 ### Comparison with Axis2/Java
 
diff --git a/fuzz/corpus/finbench/compose_bad_vol.json b/fuzz/corpus/finbench/compose_bad_vol.json
new file mode 100644
index 000000000..8c40c0990
--- /dev/null
+++ b/fuzz/corpus/finbench/compose_bad_vol.json
@@ -0,0 +1 @@
+{"volatilities":[0.2,-0.1],"correlation":0.5}
diff --git a/fuzz/corpus/finbench/compose_matrix.json b/fuzz/corpus/finbench/compose_matrix.json
new file mode 100644
index 000000000..abd45e17d
--- /dev/null
+++ b/fuzz/corpus/finbench/compose_matrix.json
@@ -0,0 +1 @@
+{"volatilities":[0.25,0.30],"correlation_matrix":[[1.0,0.3],[0.3,1.0]],"asset_ids":["A","B"]}
diff --git a/fuzz/corpus/finbench/compose_not_pd.json b/fuzz/corpus/finbench/compose_not_pd.json
new file mode 100644
index 000000000..70a5e744b
--- /dev/null
+++ b/fuzz/corpus/finbench/compose_not_pd.json
@@ -0,0 +1 @@
+{"volatilities":[0.2,0.2,0.2],"correlation":-0.6}
diff --git a/fuzz/corpus/finbench/compose_uniform.json b/fuzz/corpus/finbench/compose_uniform.json
new file mode 100644
index 000000000..384fb3332
--- /dev/null
+++ b/fuzz/corpus/finbench/compose_uniform.json
@@ -0,0 +1 @@
+{"volatilities":[0.2696,0.2891,0.3241,0.2737,0.1844],"correlation":0.8,"request_id":"compose-uniform"}
diff --git a/fuzz/corpus/finbench/covret_constant.json b/fuzz/corpus/finbench/covret_constant.json
new file mode 100644
index 000000000..7c9dbb6a9
--- /dev/null
+++ b/fuzz/corpus/finbench/covret_constant.json
@@ -0,0 +1 @@
+{"returns":[[0.01,0.01,0.01],[0.02,-0.01,0.0]]}
diff --git a/fuzz/corpus/finbench/covret_flat.json b/fuzz/corpus/finbench/covret_flat.json
new file mode 100644
index 000000000..5ea5a3610
--- /dev/null
+++ b/fuzz/corpus/finbench/covret_flat.json
@@ -0,0 +1 @@
+{"returns":[0.01,-0.02,0.015,0.005,-0.01,0.02],"n_assets":2,"n_obs":3}
diff --git a/fuzz/corpus/finbench/covret_nested.json b/fuzz/corpus/finbench/covret_nested.json
new file mode 100644
index 000000000..295107ea0
--- /dev/null
+++ b/fuzz/corpus/finbench/covret_nested.json
@@ -0,0 +1 @@
+{"returns":[[0.010,-0.020,0.015,0.000,0.004,-0.007],[0.005,-0.010,0.020,-0.005,0.002,-0.003]],"n_periods_per_year":252}
diff --git a/fuzz/corpus/finbench/covret_nulls.json b/fuzz/corpus/finbench/covret_nulls.json
new file mode 100644
index 000000000..5326b457a
--- /dev/null
+++ b/fuzz/corpus/finbench/covret_nulls.json
@@ -0,0 +1 @@
+{"returns":[[0.01,null,0.02,0.0,-0.01],[0.005,0.01,null,-0.005,0.002]],"n_periods_per_year":12,"check_positive_definite":true}
diff --git a/fuzz/corpus/finbench/mc_correlated_merton.json b/fuzz/corpus/finbench/mc_correlated_merton.json
new file mode 100644
index 000000000..68644890d
--- /dev/null
+++ b/fuzz/corpus/finbench/mc_correlated_merton.json
@@ -0,0 +1 @@
+{"n_simulations":2000,"n_periods":252,"initial_value":1000000,"expected_return":0.08,"random_seed":12345,"model":"merton","jump_intensity":1.0,"jump_mean":-0.03,"jump_vol":0.05,"weights":[0.2,0.2,0.2,0.2,0.2],"covariance_matrix":[0.072678,0.062359,0.069907,0.059024,0.039764,0.062359,0.083602,0.074977,0.063305,0.042648,0.069907,0.074977,0.105066,0.070967,0.04781,0.059024,0.063305,0.070967,0.074899,0.040367,0.039764,0.042648,0.04781,0.040367,0.033994]}
diff --git a/fuzz/corpus/finbench/mc_correlated_not_pd.json b/fuzz/corpus/finbench/mc_correlated_not_pd.json
new file mode 100644
index 000000000..40949f8aa
--- /dev/null
+++ b/fuzz/corpus/finbench/mc_correlated_not_pd.json
@@ -0,0 +1 @@
+{"n_simulations":1000,"weights":[0.5,0.5],"covariance_matrix":[0.04,-0.05,-0.05,0.04]}
diff --git a/fuzz/corpus/finbench/mc_empty.json b/fuzz/corpus/finbench/mc_empty.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/fuzz/corpus/finbench/mc_empty.json
@@ -0,0 +1 @@
+{}
diff --git a/fuzz/corpus/finbench/mc_negative_weight.json b/fuzz/corpus/finbench/mc_negative_weight.json
new file mode 100644
index 000000000..83df62c3f
--- /dev/null
+++ b/fuzz/corpus/finbench/mc_negative_weight.json
@@ -0,0 +1 @@
+{"n_simulations":1000,"weights":[1.5,-0.5],"covariance_matrix":[0.04,0.01,0.01,0.04]}
diff --git a/fuzz/corpus/finbench/mc_scalar.json b/fuzz/corpus/finbench/mc_scalar.json
new file mode 100644
index 000000000..5eb18d464
--- /dev/null
+++ b/fuzz/corpus/finbench/mc_scalar.json
@@ -0,0 +1 @@
+{"n_simulations":2000,"n_periods":252,"initial_value":1000000,"expected_return":0.08,"volatility":0.25,"random_seed":777,"percentiles":[0.01,0.05,0.1]}
diff --git a/fuzz/corpus/finbench/pv_april_annual.json b/fuzz/corpus/finbench/pv_april_annual.json
new file mode 100644
index 000000000..59d738cd9
--- /dev/null
+++ b/fuzz/corpus/finbench/pv_april_annual.json
@@ -0,0 +1,8 @@
+{"n_assets":5,"weights":[0.25,0.25,0.20,0.15,0.15],
+ "covariance_matrix":[0.0590,0.0123,0.0318,0.0160,-0.0051,
+                      0.0123,0.0597,0.0315,0.0191,0.0036,
+                      0.0318,0.0315,0.0987,0.0262,-0.0083,
+                      0.0160,0.0191,0.0262,0.0480,0.0003,
+                     -0.0051,0.0036,-0.0083,0.0003,0.0278],
+ "normalize_weights":true,
+ "n_periods_per_year":1}
diff --git a/fuzz/corpus/finbench/pv_asymmetric.json b/fuzz/corpus/finbench/pv_asymmetric.json
new file mode 100644
index 000000000..d1f2113d5
--- /dev/null
+++ b/fuzz/corpus/finbench/pv_asymmetric.json
@@ -0,0 +1 @@
+{"n_assets":2,"weights":[0.5,0.5],"covariance_matrix":[0.04,0.010,0.012,0.04]}
diff --git a/fuzz/corpus/finbench/pv_daily_basis.json b/fuzz/corpus/finbench/pv_daily_basis.json
new file mode 100644
index 000000000..b9bea462b
--- /dev/null
+++ b/fuzz/corpus/finbench/pv_daily_basis.json
@@ -0,0 +1 @@
+{"n_assets":5,"weights":[0.2,0.2,0.2,0.2,0.2],"covariance_matrix":[0.072678,0.05155,0.057986,0.031242,0.014478,0.05155,0.083602,0.054191,0.03294,0.015999,0.057986,0.054191,0.105066,0.027568,0.008902,0.031242,0.03294,0.027568,0.074899,0.016828,0.014478,0.015999,0.008902,0.016828,0.033994],"normalize_weights":true,"n_periods_per_year":252}
diff --git a/fuzz/corpus/finbench/pv_five_asset_equal.json b/fuzz/corpus/finbench/pv_five_asset_equal.json
new file mode 100644
index 000000000..6f3cf5556
--- /dev/null
+++ b/fuzz/corpus/finbench/pv_five_asset_equal.json
@@ -0,0 +1 @@
+{"n_assets": 5, "weights": [0.2, 0.2, 0.2, 0.2, 0.2], "covariance_matrix": [0.072678, 0.05155, 0.057986, 0.031242, 0.014478, 0.05155, 0.083602, 0.054191, 0.03294, 0.015999, 0.057986, 0.054191, 0.105066, 0.027568, 0.008902, 0.031242, 0.03294, 0.027568, 0.074899, 0.016828, 0.014478, 0.015999, 0.008902, 0.016828, 0.033994], "normalize_weights": true, "n_periods_per_year": 1}
\ No newline at end of file
diff --git a/fuzz/corpus/finbench/pv_nested_shape.json b/fuzz/corpus/finbench/pv_nested_shape.json
new file mode 100644
index 000000000..be3dda4e2
--- /dev/null
+++ b/fuzz/corpus/finbench/pv_nested_shape.json
@@ -0,0 +1 @@
+{"n_assets":2,"weights":[[0.5],[0.5]],"covariance_matrix":[[0.04,0.01],[0.01,0.04]],"normalize_weights":true}
diff --git a/fuzz/corpus/finbench/pv_not_psd.json b/fuzz/corpus/finbench/pv_not_psd.json
new file mode 100644
index 000000000..616292579
--- /dev/null
+++ b/fuzz/corpus/finbench/pv_not_psd.json
@@ -0,0 +1 @@
+{"n_assets":2,"weights":[0.5,0.5],"covariance_matrix":[0.04,-0.05,-0.05,0.04]}
diff --git a/fuzz/finbench.dict b/fuzz/finbench.dict
new file mode 100644
index 000000000..53809661f
--- /dev/null
+++ b/fuzz/finbench.dict
@@ -0,0 +1,31 @@
+"n_assets"
+"weights"
+"covariance_matrix"
+"normalize_weights"
+"n_periods_per_year"
+"request_id"
+"volatilities"
+"correlation"
+"correlation_matrix"
+"check_positive_definite"
+"asset_ids"
+"returns"
+"n_obs"
+"n_simulations"
+"n_periods"
+"initial_value"
+"expected_return"
+"expected_returns"
+"volatility"
+"random_seed"
+"percentiles"
+"model"
+"gbm"
+"merton"
+"jump_intensity"
+"jump_mean"
+"jump_vol"
+"null"
+"NaN"
+"1e308"
+"-1e308"
diff --git a/fuzz/oss-fuzz/build.sh b/fuzz/oss-fuzz/build.sh
index af55f6b98..f30719a1f 100755
--- a/fuzz/oss-fuzz/build.sh
+++ b/fuzz/oss-fuzz/build.sh
@@ -87,6 +87,24 @@ $CC $CFLAGS $AXIS2_INCLUDES -I/usr/include/json-c \
     $LIB_FUZZING_ENGINE \
     $AXIS2_LIBS $JSONC_STATIC $SYS_LIBS_CORE ${LDFLAGS}
 
+# Compile the financial-benchmark service fuzzer (application-level: the
+# request->compute->response path of portfolioVariance, composeCovariance,
+# covarianceFromReturns and monteCarlo). The service source is compiled into
+# the fuzzer rather than linked from the sample's library so the target does
+# not depend on the samples being built. -fsigned-char matches the project's
+# configure flags (axis2_char_t is plain char; see docs/HTTP2_ANDROID.md).
+FINBENCH_SRC=samples/user_guide/financial-benchmark-service/src
+$CC $CFLAGS $AXIS2_INCLUDES -I/usr/include/json-c \
+    -I$WORK/install/include/axis2-2.0.0 \
+    -I$FINBENCH_SRC \
+    -std=gnu99 -fsigned-char \
+    -fsanitize=address \
+    fuzz/fuzz_finbench.c \
+    $FINBENCH_SRC/financial_benchmark_service.c \
+    -o $OUT/fuzz_finbench \
+    $LIB_FUZZING_ENGINE \
+    $AXIS2_LIBS $JSONC_STATIC $SYS_LIBS_CORE ${LDFLAGS}
+
 # Copy seed corpora
 for corpus_dir in xml json http url; do
     if [ -d "fuzz/corpus/${corpus_dir}" ]; then
@@ -103,3 +121,14 @@ fi
 if [ -f "$OUT/fuzz_json_parser_seed_corpus.zip" ]; then
     cp $OUT/fuzz_json_parser_seed_corpus.zip $OUT/fuzz_json_reader_seed_corpus.zip
 fi
+
+# Financial-benchmark fuzzer: its own corpus (one valid or deliberately
+# invalid request per operation, so every refusal branch is reached from the
+# start) and a dictionary of the JSON keys, which libFuzzer would otherwise
+# have to discover byte by byte.
+if [ -d "fuzz/corpus/finbench" ]; then
+    zip -j $OUT/fuzz_finbench_seed_corpus.zip fuzz/corpus/finbench/* 2>/dev/null || true
+fi
+if [ -f "fuzz/finbench.dict" ]; then
+    cp fuzz/finbench.dict $OUT/fuzz_finbench.dict
+fi


(axis-axis2-c-core) 07/07: Refuse an annualized matrix labelled with n_periods_per_year > 1

Posted by ro...@apache.org.
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 373f5cea900dc78cd8dbaf6a9a09b480b3a317b4
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Tue Sep 22 13:56:35 2026 -1000

    Refuse an annualized matrix labelled with n_periods_per_year > 1
    
    A covarianceFromReturns response passed back to portfolioVariance carries
    the factor it already applied as n_periods_per_year beside
    covariance_basis "annualized", which annualized the matrix twice.
    That contradiction is now refused instead of guessed.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
---
 .../src/financial_benchmark_service.c              | 24 ++++++++++++++++++++++
 .../src/financial_benchmark_service.h              |  9 ++++++++
 .../financial-benchmark-service/src/finbench_mcp.c |  3 ++-
 3 files changed, 35 insertions(+), 1 deletion(-)

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 c79329c41..560af3d49 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
@@ -338,6 +338,17 @@ finbench_portfolio_variance_request_create_from_json(
         request->n_periods_per_year = 1;
     }
 
+    /* covarianceFromReturns echoes the factor it already applied as
+     * n_periods_per_year beside covariance_basis "annualized". Taking that
+     * echo as this matrix's basis would annualize it twice, so the pair is
+     * flagged and refused in the calculation. */
+    if (request->n_periods_per_year != 1 &&
+        json_object_object_get_ex(json_obj, "covariance_basis", &value_obj) &&
+        json_object_is_type(value_obj, json_type_string) &&
+        strcmp(json_object_get_string(value_obj), "annualized") == 0) {
+        request->basis_conflict = AXIS2_TRUE;
+    }
+
     json_object_put(json_obj);
     return request;
 }
@@ -458,6 +469,19 @@ finbench_calculate_portfolio_variance(
 
     n = request->n_assets;
 
+    if (request->basis_conflict) {
+        char err_buf[256];
+        snprintf(err_buf, sizeof(err_buf),
+            "covariance_basis is \"annualized\" but n_periods_per_year is %d. "
+            "An annualized matrix takes n_periods_per_year 1 (or omit it); "
+            "a per-period matrix must not be labelled annualized.",
+            request->n_periods_per_year);
+        response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+        response->error_message = axutil_strdup(env, err_buf);
+        AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI, "FinBench portfolioVariance: %s", err_buf);
+        return response;
+    }
+
     /* -----------------------------------------------------------------------
      * Dimension validation — the kind of check a Python quant will probe
      * immediately. Mismatched or absent arrays silently produce wrong variance
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 61debf83e..4cd6ffd86 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
@@ -224,6 +224,15 @@ typedef struct finbench_portfolio_variance_request
      */
     int n_periods_per_year;
 
+    /**
+     * Set by the parser when the request says covariance_basis "annualized"
+     * but also carries n_periods_per_year other than 1 — the shape of a
+     * covarianceFromReturns response passed straight back in, whose
+     * n_periods_per_year is the factor already applied. Refused rather than
+     * guessed: honouring either field silently could be the wrong one.
+     */
+    axis2_bool_t basis_conflict;
+
 } finbench_portfolio_variance_request_t;
 
 /**
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 215b74b89..f1fc91a30 100644
--- a/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
+++ b/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
@@ -81,7 +81,8 @@ static const char SCHEMA_PORTFOLIO_VARIANCE[] =
         "\"n_periods_per_year\":{\"type\":\"integer\",\"default\":1,"
             "\"description\":\"Periods per year of the INPUT matrix. Default 1 = the matrix is already "
                 "annualized (leave it for any matrix produced by this service). Only for a per-period "
-                "matrix: 252 daily, 52 weekly, 12 monthly. The response echoes covariance_basis\"},"
+                "matrix: 252 daily, 52 weekly, 12 monthly. The response echoes covariance_basis. "
+                "Refused if the request also says covariance_basis \\\"annualized\\\"\"},"
         "\"request_id\":{\"type\":\"string\","
             "\"description\":\"Optional identifier echoed in the response for request tracing\"}"
     "},"


(axis-axis2-c-core) 01/07: Add covarianceFromReturns to the financial benchmark sample

Posted by ro...@apache.org.
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 0446624d3e5577a986852278da1cf314d1732f4c
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Sun Sep 20 12:26:26 2026 -1000

    Add covarianceFromReturns to the financial benchmark sample
    
    Sample covariance of a return matrix, Bessel-corrected and annualised by
    n_periods_per_year, producing the flat row-major matrix portfolioVariance
    and the correlated monteCarlo already take. Missing observations are
    complete-case and observations_per_pair reports what each pair would have
    had, so thin data is visible rather than silently averaged over. Capped at
    50 assets and 5000 observations because the request itself is O(n*m).
    
    Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
 docs/HTTP2_ANDROID.md                              |   9 +-
 docs/MCP_EXAMPLES.md                               |  62 ++
 fuzz/fuzz_finbench.c                               |  34 +
 .../financial-benchmark-service/README.md          |  71 +-
 .../financial-benchmark-service/services.xml       |  74 ++-
 .../src/financial_benchmark_service.c              | 724 ++++++++++++++++++++-
 .../src/financial_benchmark_service.h              | 211 ++++++
 .../src/financial_benchmark_service_handler.c      |   6 +
 .../financial-benchmark-service/src/finbench_mcp.c |  45 +-
 .../financial-benchmark-service/src/finbench_mcp.h |   6 +-
 10 files changed, 1224 insertions(+), 18 deletions(-)

diff --git a/docs/HTTP2_ANDROID.md b/docs/HTTP2_ANDROID.md
index 6d33b1963..59cfb8b70 100644
--- a/docs/HTTP2_ANDROID.md
+++ b/docs/HTTP2_ANDROID.md
@@ -393,8 +393,11 @@ from the message context. Route by that field first and infer from the
 request's shape only as a fallback: two operations can share a field name (a
 correlated `monteCarlo` request and a `portfolioVariance` request both carry
 `"weights"`), and a shape rule that decided on `"weights"` alone sent the
-first to the second. Neither `CameraControlService` nor `AudioSearchService`
-reads `"operation"`, so the added field is inert for them.
+first to the second. `CameraControlService` and `AudioSearchService` name
+their operations with `"action"` and took only that key until 2026-09-20, so
+every HTTP call to them failed once the engine started naming the operation
+itself; both now accept either key. A service that reads neither is unaffected
+by the added field.
 
 **Operation trust model (since 2026-09-20).** The URL is authoritative. Any
 authorization in front of the server — an httpd `<Location>` block, a
@@ -618,7 +621,7 @@ repository's registry**. What it does touch, using `composeCovariance` on
 
 | 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.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`, as `covarianceFromReturns` is the only one that carries `returns`) |
 | `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 |
diff --git a/docs/MCP_EXAMPLES.md b/docs/MCP_EXAMPLES.md
index c512bfa13..80ba6ed72 100644
--- a/docs/MCP_EXAMPLES.md
+++ b/docs/MCP_EXAMPLES.md
@@ -255,6 +255,64 @@ 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.
 
+### Covariance From Returns — the Matrix From Data, Not Assumptions
+
+`composeCovariance` builds Σ from a view. `covarianceFromReturns` builds it
+from a history: the sample covariance of the returns themselves, annualized,
+in the same flat row-major shape the other operations take.
+
+```bash
+# Three assets, ten daily returns each. Two move together; the third hedges.
+curl -sk --http2 -X POST -H 'Content-Type: application/json' \
+    -d '{"returns":[[0.0104,-0.0071,0.0043,0.0128,-0.0195,0.0061,0.0037,-0.0044,0.0090,-0.0012],
+                    [0.0088,-0.0053,0.0021,0.0141,-0.0168,0.0032,0.0055,-0.0071,0.0102,0.0004],
+                    [-0.0021,0.0034,-0.0008,-0.0046,0.0072,0.0011,-0.0025,0.0040,-0.0033,0.0018]],
+         "n_periods_per_year":252,
+         "asset_ids":["TECH1","TECH2","DEFENSIVE"]}' \
+    https://10.10.10.10/services/FinancialBenchmarkService/covarianceFromReturns
+```
+
+```json
+{
+  "status": "SUCCESS",
+  "n_assets": 3,
+  "n_obs_provided": 10,
+  "n_obs_used": 10,
+  "n_periods_per_year": 252.0,
+  "covariance_matrix": [0.023911, 0.022047, -0.008585, 0.022047, 0.021372, -0.008454, -0.008585, -0.008454, 0.003512],
+  "correlation_matrix": [1.0, 0.9753, -0.9369, 0.9753, 1.0, -0.9757, -0.9369, -0.9757, 1.0],
+  "volatilities": [0.1546, 0.1462, 0.0593],
+  "mean_returns": [0.3553, 0.3805, 0.1058],
+  "observations_per_pair": [10, 10, 10, 10, 10, 10, 10, 10, 10],
+  "positive_definite": true,
+  "cholesky_failed_at": -1,
+  "min_pivot": 0.00015285,
+  "calc_time_us": 129
+}
+```
+
+That matrix into `portfolioVariance` with weights `[0.4, 0.4, 0.2]` and
+`"n_periods_per_year": 1` gives variance 0.011715, volatility 0.1082 — the
+hedge earning its place. Ten observations is a toy sample, and the annualized
+`mean_returns` show it: 35% and 38% are ten days multiplied by 252, not a
+forecast.
+
+Gaps are complete-case. Null out two observations of the third asset and the
+estimator drops those dates for every asset, reporting both counts:
+
+```json
+{"n_obs_used": 8,
+ "observations_per_pair": [10, 10, 8,  10, 10, 8,  8, 8, 8],
+ "covariance_matrix": [0.010575, ...]}
+```
+
+The first pair still had ten observations of its own — `observations_per_pair`
+is how a caller sees what completeness cost. A history where those numbers
+diverge badly wants pairwise completion and shrinkage, which this operation
+does not do and does not pretend to: it is the textbook estimator, checked with
+the same Cholesky as `composeCovariance` and refused when the sample is
+rank-deficient.
+
 ### Correlated Book — composeCovariance into monteCarlo
 
 The point of building Σ on the device is to simulate the book on the device.
@@ -499,6 +557,7 @@ curl -k --http2 -s \
   "operations": [
     "portfolioVariance",
     "composeCovariance",
+    "covarianceFromReturns",
     "monteCarlo",
     "scenarioAnalysis",
     "metadata"
@@ -967,6 +1026,9 @@ import math, json
 
 # 254 daily log returns computed from 255 closing prices (April 2025 - April 2026)
 # Annualized covariance: cov(r_i, r_j) * 252
+#
+# Since this session was recorded the service does this step itself:
+# covarianceFromReturns takes the return matrix and returns the same Sigma.
 
 # Real annualized volatilities (from market data, as of 2026-04-07):
 #   MSFT: 26.3%   AAPL: 31.2%   AMZN: 34.7%   JPM: 25.2%   JNJ: 17.4%
diff --git a/fuzz/fuzz_finbench.c b/fuzz/fuzz_finbench.c
index 5e71c6269..c32b6f9b4 100644
--- a/fuzz/fuzz_finbench.c
+++ b/fuzz/fuzz_finbench.c
@@ -31,6 +31,11 @@
  *   Tests: out-of-range and NaN correlations, asymmetric and ragged
  *   matrices, non-positive pivots, uniform rho below -1/(n-1)
  *
+ * - covarianceFromReturns: sample covariance of a return matrix
+ *   Tests: 2D and flat shape inference, nulls as missing values, ragged
+ *   rows, non-numeric elements, samples too small for Bessel's correction,
+ *   zero-variance assets, rank-deficient samples
+ *
  * - monteCarlo: GBM simulation with exp() overflow guard
  *   Tests: extreme volatility → exp() overflow, zero volatility,
  *   negative expected return, 1-simulation edge case, percentile
@@ -146,6 +151,35 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
         }
     }
 
+    /*
+     * Exercise 1c: covarianceFromReturns parsing + sample covariance.
+     * This hits the 2D/flat shape inference, the null-as-missing path, the
+     * complete-case mask, the zero-variance and rank-deficiency refusals,
+     * and the same Cholesky pivot check from a different input shape.
+     */
+    {
+        finbench_covariance_from_returns_request_t *req =
+            finbench_covariance_from_returns_request_create_from_json(env, json_str);
+        if (req) {
+            /* Parsing is fuzzed for every input; the estimator itself runs
+             * only on small samples. It is O(n_assets^2 * n_obs), and the
+             * dimensions must not be trimmed here: they are what the shape
+             * validation checks against. */
+            finbench_covariance_from_returns_response_t *resp =
+                ((long)req->n_assets * req->n_obs <= 4096)
+                    ? finbench_covariance_from_returns(env, req) : NULL;
+            if (resp) {
+                axis2_char_t *json_out =
+                    finbench_covariance_from_returns_response_to_json(resp, env);
+                if (json_out) {
+                    AXIS2_FREE(env->allocator, json_out);
+                }
+                finbench_covariance_from_returns_response_free(resp, env);
+            }
+            finbench_covariance_from_returns_request_free(req, env);
+        }
+    }
+
     /*
      * Exercise 2: Monte Carlo request parsing + GBM simulation.
      * This hits the exp() overflow guard, percentile clamping,
diff --git a/samples/user_guide/financial-benchmark-service/README.md b/samples/user_guide/financial-benchmark-service/README.md
index d65d3f004..3f7f881bb 100644
--- a/samples/user_guide/financial-benchmark-service/README.md
+++ b/samples/user_guide/financial-benchmark-service/README.md
@@ -181,11 +181,78 @@ capped at 500 assets regardless: the check is O(n³), and because the uniform
 form builds two n×n matrices from a request of a few dozen bytes, the cap also
 bounds how large a reply a tiny request can produce.
 
-### 4. Scenario Analysis (`/scenarioAnalysis`)
+### 4. Covariance From Returns (`/covarianceFromReturns`)
+
+Builds the same matrix from data instead of assumptions: the sample covariance
+of a return history, annualised, ready for `portfolioVariance` and for the
+correlated `monteCarlo`.
+
+**Formula**: Σ_ij = Σ_t (r_it − m_i)(r_jt − m_j) / (M − 1) × `n_periods_per_year`,
+over the M observations where every asset is present (Pearson 1896; Bessel's
+correction).
+
+Missing data is complete-case: an observation where any asset is `null` is
+dropped for every asset, so one common sample backs every entry and the result
+is positive semi-definite by construction. `observations_per_pair` reports what
+each pair would have had on its own, which is how a caller sees that dropping
+was expensive. The same Cholesky check as `composeCovariance` runs on the
+result and refuses a rank-deficient sample — fewer complete observations than
+assets, or two series that move together exactly.
+
+**Example Request** (two assets, daily returns):
+```json
+{
+    "returns": [[0.0021, -0.0043, 0.0117, null, 0.0005],
+                [0.0017, -0.0038, 0.0090, 0.0012, 0.0002]],
+    "n_periods_per_year": 252,
+    "asset_ids": ["A", "B"]
+}
+```
+
+**Example Response**:
+```json
+{
+    "status": "SUCCESS",
+    "n_assets": 2,
+    "n_obs_provided": 5,
+    "n_obs_used": 4,
+    "n_periods_per_year": 252.0,
+    "covariance_matrix": [0.01134336, 0.00903504, 0.00903504, 0.00720447],
+    "correlation_matrix": [1.0, 0.9994434, 0.9994434, 1.0],
+    "volatilities": [0.1065052, 0.0848791],
+    "mean_returns": [0.63, 0.4473],
+    "observations_per_pair": [4, 4, 4, 5],
+    "positive_definite": true,
+    "cholesky_failed_at": -1,
+    "calc_time_us": 14
+}
+```
+
+The `mean_returns` above are annualised from five toy observations, which is
+why they read as 63% and 45%: the factor multiplies whatever sample it is
+given, and a five-day sample is not a year.
+
+Pass `n_periods_per_year: 1` to keep the input's own basis; pass 12 for
+monthly data. The annualised matrix goes to `portfolioVariance` with
+`n_periods_per_year: 1`, since it is already annual.
+
+**Refusals** name the field: a ragged `returns` row, an element that is neither
+a number nor `null`, a non-finite value, fewer than two complete observations,
+an asset with zero variance, or a Cholesky failure. Caps are 50 assets and
+5,000 observations per asset: unlike the other operations the request itself is
+O(n_assets × n_obs) numbers, and a wider book belongs in a file the service
+reads rather than in a request body.
+
+**What this is not.** It is the textbook estimator, with no shrinkage and no
+pairwise completion. For a history with real gaps, or for a covariance that
+feeds an optimiser, a library that has both earns its keep; this operation is
+here so that a device holding the returns can produce the matrix without one.
+
+### 5. 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.
 
-### 5. Service Metadata (`/metadata`)
+### 6. 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 95eb028dd..1bb77f5f1 100644
--- a/samples/user_guide/financial-benchmark-service/services.xml
+++ b/samples/user_guide/financial-benchmark-service/services.xml
@@ -42,6 +42,7 @@
             Operations:
             - portfolioVariance: O(n²) covariance matrix calculation
             - composeCovariance: Σ = D·R·D from volatilities + correlations, Cholesky-checked
+            - covarianceFromReturns: sample Σ from a return history, complete-case
             - monteCarlo: Compute-intensive VaR simulation
             - scenarioAnalysis: Hash table vs linear lookup comparison
             - metadata: Service information and device stats
@@ -203,6 +204,77 @@
             <parameter name="responseType">application/json</parameter>
         </operation>
 
+        <!-- ================================================================
+             Operation: covarianceFromReturns
+
+             Sample covariance from a return history: the textbook estimator
+             (Pearson, Bessel-corrected, annualised) that turns a CSV of
+             returns into the matrix portfolioVariance and monteCarlo take.
+             Missing observations are complete-case; the reply reports what
+             each pair would have had so thin data is visible.
+             ================================================================ -->
+        <operation name="covarianceFromReturns">
+            <parameter name="mcpDescription">Sample covariance matrix from a return history, annualised by n_periods_per_year (252 for daily). Returns the flat row-major covariance_matrix that portfolioVariance accepts, the implied correlations, per-asset volatilities and annualised means, and observations per pair. Missing data is complete-case; a Cholesky check refuses a rank-deficient sample.</parameter>
+            <parameter name="mcpReadOnly">true</parameter>
+            <parameter name="mcpIdempotent">true</parameter>
+            <parameter name="mcpInputSchema">{
+                "type": "object",
+                "required": ["returns"],
+                "properties": {
+                    "returns":                 {"type": "array",
+                                                "description": "Periodic returns: n_assets arrays of n_obs numbers, or one flat row-major array with n_assets and n_obs given. Log or simple returns, not prices. A null element means that observation is missing."},
+                    "n_assets":                {"type": "integer", "minimum": 1, "maximum": 50,
+                                                "description": "Number of assets (max 50: the request carries n_assets×n_obs numbers). Inferred from a nested returns array."},
+                    "n_obs":                   {"type": "integer", "minimum": 2, "maximum": 5000,
+                                                "description": "Observations per asset (max 5000, about twenty years of daily data). Inferred from the first row."},
+                    "n_periods_per_year":      {"type": "number",  "default": 252,
+                                                "description": "Annualisation factor: Σ is multiplied by it and the means with it. 252 daily, 12 monthly, 1 to keep the input's own basis."},
+                    "check_positive_definite": {"type": "boolean", "default": true,
+                                                "description": "Cholesky check. Refuses a rank-deficient sample (fewer complete observations than assets, or duplicated series)."},
+                    "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>
+                Sample covariance: Σ_ij = Σ_t (r_it − m_i)(r_jt − m_j) / (M − 1) × n_periods_per_year,
+                over the M observations where every asset is present.
+
+                Example JSON Request:
+                {
+                    "returns": [[0.0021, -0.0043, ...], [0.0017, -0.0038, ...]],
+                    "n_periods_per_year": 252,
+                    "asset_ids": ["MSFT", "AAPL"]
+                }
+
+                Example JSON Response:
+                {
+                    "status": "SUCCESS",
+                    "n_assets": 2,
+                    "n_obs_provided": 2508,
+                    "n_obs_used": 2508,
+                    "n_periods_per_year": 252,
+                    "covariance_matrix": [0.072673, 0.041958, ...],
+                    "correlation_matrix": [1, 0.538, ...],
+                    "volatilities": [0.2696, 0.2891],
+                    "mean_returns": [0.2447, 0.2163],
+                    "observations_per_pair": [2508, 2508, ...],
+                    "positive_definite": true,
+                    "calc_time_us": 142
+                }
+
+                This is the textbook estimator: no shrinkage, no pairwise
+                completion. A history with real gaps deserves a library that
+                has both; observations_per_pair shows when that matters.
+            </description>
+            <messageReceiver class="axis2_json_rpc_msg_recv"/>
+
+            <parameter name="httpMethod">POST</parameter>
+            <parameter name="httpPath">/covarianceFromReturns</parameter>
+            <parameter name="contentType">application/json</parameter>
+            <parameter name="responseType">application/json</parameter>
+        </operation>
+
         <!-- ================================================================
              Operation: monteCarlo
 
@@ -409,7 +481,7 @@
                 {
                     "service_name": "FinancialBenchmarkService",
                     "version": "1.0.0",
-                    "operations": ["portfolioVariance", "composeCovariance", "monteCarlo", ...],
+                    "operations": ["portfolioVariance", "composeCovariance", "covarianceFromReturns", "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 39b10bc9c..862b05f24 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
@@ -1322,6 +1322,697 @@ finbench_compose_covariance_json_only(
     return json_response;
 }
 
+/* ============================================================================
+ * Covariance From Returns Implementation
+ * ============================================================================
+ */
+
+/* Drop every output array. Same reason as compose_drop_outputs: the
+ * serializer emits whatever is non-NULL, so a half-filled matrix must never
+ * survive a failure. */
+static void
+cfr_drop_outputs(
+    const axutil_env_t *env,
+    finbench_covariance_from_returns_response_t *response)
+{
+    if (response->covariance_matrix) {
+        AXIS2_FREE(env->allocator, response->covariance_matrix);
+        response->covariance_matrix = NULL;
+    }
+    if (response->correlation_matrix) {
+        AXIS2_FREE(env->allocator, response->correlation_matrix);
+        response->correlation_matrix = NULL;
+    }
+    if (response->volatilities) {
+        AXIS2_FREE(env->allocator, response->volatilities);
+        response->volatilities = NULL;
+    }
+    if (response->mean_returns) {
+        AXIS2_FREE(env->allocator, response->mean_returns);
+        response->mean_returns = NULL;
+    }
+    if (response->observations_per_pair) {
+        AXIS2_FREE(env->allocator, response->observations_per_pair);
+        response->observations_per_pair = NULL;
+    }
+}
+
+/* Set a FAILED status with a formatted message; returns the response for
+ * convenient "return cfr_fail(...)" use. */
+static finbench_covariance_from_returns_response_t *
+cfr_fail(
+    const axutil_env_t *env,
+    finbench_covariance_from_returns_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 covarianceFromReturns: %s", buf);
+    return response;
+}
+
+/* Store one returns element. A JSON null becomes NaN ("not observed"); a
+ * number is taken as is; anything else is recorded as the first bad element
+ * so the compute step can name it. Returns nothing: validation is deferred so
+ * that one malformed cell produces a sentence, not a NULL request. */
+static void
+cfr_store_element(
+    finbench_covariance_from_returns_request_t *request,
+    json_object *elem,
+    int row,
+    int col)
+{
+    double *slot = &request->returns[(size_t)row * request->n_obs + col];
+
+    if (!elem || json_object_is_type(elem, json_type_null)) {
+        *slot = NAN;
+        return;
+    }
+    if (json_object_is_type(elem, json_type_double) ||
+        json_object_is_type(elem, json_type_int)) {
+        *slot = json_object_get_double(elem);
+        return;
+    }
+    *slot = NAN;
+    if (request->bad_value_row < 0) {
+        request->bad_value_row = row;
+        request->bad_value_col = col;
+    }
+}
+
+AXIS2_EXTERN finbench_covariance_from_returns_request_t* AXIS2_CALL
+finbench_covariance_from_returns_request_create_from_json(
+    const axutil_env_t *env,
+    const axis2_char_t *json_string)
+{
+    finbench_covariance_from_returns_request_t *request = NULL;
+    json_object *json_obj = NULL;
+    json_object *value_obj = NULL;
+    json_object *array_obj = NULL;
+    json_object *first_elem = NULL;
+    int outer_len = 0;
+    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 covarianceFromReturns: Failed to parse JSON request");
+        return NULL;
+    }
+
+    if (!json_object_object_get_ex(json_obj, "returns", &array_obj) ||
+        !json_object_is_type(array_obj, json_type_array)) {
+        AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+            "FinBench covarianceFromReturns: \"returns\" must be an array");
+        json_object_put(json_obj);
+        return NULL;
+    }
+
+    request = AXIS2_MALLOC(env->allocator,
+        sizeof(finbench_covariance_from_returns_request_t));
+    if (!request) {
+        json_object_put(json_obj);
+        return NULL;
+    }
+    memset(request, 0, sizeof(finbench_covariance_from_returns_request_t));
+    request->check_positive_definite = AXIS2_TRUE;
+    request->n_periods_per_year = FINBENCH_DEFAULT_PERIODS_PER_YEAR;
+    request->ragged_at = -1;
+    request->bad_value_row = -1;
+    request->bad_value_col = -1;
+
+    /* Shape: a 2D array is n_assets rows of n_obs; a flat array needs both
+     * dimensions named, exactly as the covariance matrix inputs do. */
+    outer_len = json_object_array_length(array_obj);
+    first_elem = (outer_len > 0) ? json_object_array_get_idx(array_obj, 0) : NULL;
+    request->rows_provided = outer_len;
+
+    if (first_elem && json_object_is_type(first_elem, json_type_array)) {
+        request->n_assets = outer_len;
+        request->n_obs = json_object_array_length(first_elem);
+        request->obs_provided = request->n_obs;
+    } else {
+        request->flat_shape = AXIS2_TRUE;
+        if (json_object_object_get_ex(json_obj, "n_assets", &value_obj)) {
+            request->n_assets = json_object_get_int(value_obj);
+        }
+        if (json_object_object_get_ex(json_obj, "n_obs", &value_obj)) {
+            request->n_obs = json_object_get_int(value_obj);
+        }
+        request->obs_provided = outer_len;
+    }
+
+    if (request->n_assets <= 0 || request->n_assets > FINBENCH_MAX_RET_ASSETS ||
+        request->n_obs <= 0 || request->n_obs > FINBENCH_MAX_OBS) {
+        AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+            "FinBench covarianceFromReturns: Invalid dimensions n_assets=%d (max %d), "
+            "n_obs=%d (max %d)",
+            request->n_assets, FINBENCH_MAX_RET_ASSETS,
+            request->n_obs, FINBENCH_MAX_OBS);
+        AXIS2_FREE(env->allocator, request);
+        json_object_put(json_obj);
+        return NULL;
+    }
+
+    request->returns = AXIS2_MALLOC(env->allocator,
+        (size_t)request->n_assets * request->n_obs * sizeof(double));
+    if (!request->returns) {
+        AXIS2_FREE(env->allocator, request);
+        json_object_put(json_obj);
+        return NULL;
+    }
+    for (i = 0; i < request->n_assets * request->n_obs; i++) {
+        request->returns[i] = NAN;
+    }
+
+    if (request->flat_shape) {
+        for (i = 0; i < request->n_assets; i++) {
+            for (j = 0; j < request->n_obs; j++) {
+                int flat = i * request->n_obs + j;
+                if (flat >= outer_len) break;
+                cfr_store_element(request,
+                    json_object_array_get_idx(array_obj, flat), i, j);
+            }
+        }
+    } else {
+        for (i = 0; i < request->n_assets; i++) {
+            json_object *row = json_object_array_get_idx(array_obj, i);
+            int row_len;
+
+            if (!row || !json_object_is_type(row, json_type_array)) {
+                if (request->ragged_at < 0) request->ragged_at = i;
+                continue;
+            }
+            row_len = json_object_array_length(row);
+            if (row_len != request->n_obs && request->ragged_at < 0) {
+                request->ragged_at = i;
+            }
+            for (j = 0; j < request->n_obs && j < row_len; j++) {
+                cfr_store_element(request, json_object_array_get_idx(row, j), i, j);
+            }
+        }
+    }
+
+    if (json_object_object_get_ex(json_obj, "n_periods_per_year", &value_obj) &&
+        (json_object_is_type(value_obj, json_type_double) ||
+         json_object_is_type(value_obj, json_type_int))) {
+        request->n_periods_per_year = json_object_get_double(value_obj);
+    }
+
+    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) &&
+        (int)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_covariance_from_returns_request_free(
+    finbench_covariance_from_returns_request_t *request,
+    const axutil_env_t *env)
+{
+    if (!request || !env) return;
+
+    if (request->returns)
+        AXIS2_FREE(env->allocator, request->returns);
+    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_covariance_from_returns_response_t* AXIS2_CALL
+finbench_covariance_from_returns_response_create(const axutil_env_t *env)
+{
+    finbench_covariance_from_returns_response_t *response;
+
+    response = AXIS2_MALLOC(env->allocator,
+        sizeof(finbench_covariance_from_returns_response_t));
+    if (response) {
+        memset(response, 0, sizeof(finbench_covariance_from_returns_response_t));
+        response->cholesky_failed_at = -1;
+    }
+    return response;
+}
+
+AXIS2_EXTERN void AXIS2_CALL
+finbench_covariance_from_returns_response_free(
+    finbench_covariance_from_returns_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->mean_returns)
+        AXIS2_FREE(env->allocator, response->mean_returns);
+    if (response->observations_per_pair)
+        AXIS2_FREE(env->allocator, response->observations_per_pair);
+    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);
+}
+
+AXIS2_EXTERN finbench_covariance_from_returns_response_t* AXIS2_CALL
+finbench_covariance_from_returns(
+    const axutil_env_t *env,
+    finbench_covariance_from_returns_request_t *request)
+{
+    finbench_covariance_from_returns_response_t *response = NULL;
+    axis2_char_t *complete = NULL;
+    long start_time, end_time;
+    double factor;
+    int i, j, t, n, obs, used = 0;
+    size_t matrix_size;
+
+    response = finbench_covariance_from_returns_response_create(env);
+    if (!response) return NULL;
+
+    if (!request || !request->returns) {
+        return cfr_fail(env, response, "Invalid request parameters");
+    }
+
+    n = request->n_assets;
+    obs = request->n_obs;
+    factor = request->n_periods_per_year;
+    matrix_size = (size_t)n * n;
+    response->n_assets = n;
+    response->n_obs_provided = obs;
+    response->n_periods_per_year = factor;
+
+    /* --- Shape and range validation. Every refusal names the field. --- */
+
+    if (request->flat_shape && request->obs_provided != n * obs) {
+        return cfr_fail(env, response,
+            "returns must have exactly n_assets*n_obs = %d elements when flat; got %d.",
+            n * obs, request->obs_provided);
+    }
+    if (!request->flat_shape && request->ragged_at >= 0) {
+        return cfr_fail(env, response,
+            "returns[%d] is not an array of %d observations; every asset needs the "
+            "same observation count (complete-case handling uses nulls for gaps).",
+            request->ragged_at, obs);
+    }
+    if (request->bad_value_row >= 0) {
+        return cfr_fail(env, response,
+            "returns[%d][%d] is neither a number nor null.",
+            request->bad_value_row, request->bad_value_col);
+    }
+    if (!isfinite(factor) || factor <= 0.0) {
+        return cfr_fail(env, response,
+            "n_periods_per_year = %g must be finite and > 0 (252 for daily, 1 to keep "
+            "the input's own basis).", factor);
+    }
+
+    /* --- Complete-case mask: one sample for every pair --- */
+    complete = AXIS2_MALLOC(env->allocator, (size_t)obs * sizeof(axis2_char_t));
+    if (!complete) {
+        return cfr_fail(env, response, "Memory allocation failed for %d observations.", obs);
+    }
+    for (t = 0; t < obs; t++) {
+        complete[t] = 1;
+        for (i = 0; i < n; i++) {
+            double v = request->returns[(size_t)i * obs + t];
+            if (isnan(v)) {
+                complete[t] = 0;
+                break;
+            }
+            if (!isfinite(v)) {
+                AXIS2_FREE(env->allocator, complete);
+                return cfr_fail(env, response,
+                    "returns[%d][%d] = %g is not finite.", i, t, v);
+            }
+        }
+        if (complete[t]) used++;
+    }
+
+    if (used < 2) {
+        int u = used;
+        AXIS2_FREE(env->allocator, complete);
+        return cfr_fail(env, response,
+            "Only %d of %d observations have every asset present; the sample "
+            "covariance needs at least 2 (Bessel's correction divides by n-1).",
+            u, obs);
+    }
+    response->n_obs_used = used;
+
+    /* --- 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));
+    response->mean_returns = AXIS2_MALLOC(env->allocator, (size_t)n * sizeof(double));
+    response->observations_per_pair = AXIS2_MALLOC(env->allocator, matrix_size * sizeof(int));
+    if (!response->covariance_matrix || !response->correlation_matrix ||
+        !response->volatilities || !response->mean_returns ||
+        !response->observations_per_pair) {
+        AXIS2_FREE(env->allocator, complete);
+        cfr_drop_outputs(env, response);
+        return cfr_fail(env, response,
+            "Memory allocation failed for %d assets.", n);
+    }
+
+    start_time = get_time_us();
+
+    /* --- Means over the complete cases --- */
+    for (i = 0; i < n; i++) {
+        double sum = 0.0;
+        for (t = 0; t < obs; t++) {
+            if (complete[t]) sum += request->returns[(size_t)i * obs + t];
+        }
+        response->mean_returns[i] = sum / used;
+    }
+
+    /* --- Sigma, Bessel-corrected and annualised --- */
+    for (i = 0; i < n; i++) {
+        for (j = 0; j <= i; j++) {
+            double acc = 0.0;
+            double cov;
+            for (t = 0; t < obs; t++) {
+                if (!complete[t]) continue;
+                acc += (request->returns[(size_t)i * obs + t] - response->mean_returns[i]) *
+                       (request->returns[(size_t)j * obs + t] - response->mean_returns[j]);
+            }
+            cov = (acc / (used - 1)) * factor;
+            response->covariance_matrix[i * n + j] = cov;
+            response->covariance_matrix[j * n + i] = cov;
+        }
+    }
+
+    /* --- Observations per pair: what a pairwise estimator would have had --- */
+    for (i = 0; i < n; i++) {
+        for (j = 0; j <= i; j++) {
+            int both = 0;
+            for (t = 0; t < obs; t++) {
+                if (!isnan(request->returns[(size_t)i * obs + t]) &&
+                    !isnan(request->returns[(size_t)j * obs + t])) {
+                    both++;
+                }
+            }
+            response->observations_per_pair[i * n + j] = both;
+            response->observations_per_pair[j * n + i] = both;
+        }
+    }
+
+    AXIS2_FREE(env->allocator, complete);
+
+    /* --- Volatilities and the implied correlation --- */
+    for (i = 0; i < n; i++) {
+        double var = response->covariance_matrix[i * n + i];
+        if (!(var > 0.0)) {
+            int idx = i;
+            cfr_drop_outputs(env, response);
+            return cfr_fail(env, response,
+                "Asset %d has zero variance over the %d complete observations: it is "
+                "constant, so no correlation with it is defined.", idx, used);
+        }
+        response->volatilities[i] = sqrt(var);
+        response->mean_returns[i] *= factor;
+    }
+    for (i = 0; i < n; i++) {
+        for (j = 0; j < n; j++) {
+            double r;
+            if (i == j) {
+                /* An asset's correlation with itself is 1 by definition. The
+                 * division would give 1 minus an ulp or so, which is noise in
+                 * a field whose value is known. */
+                response->correlation_matrix[i * n + j] = 1.0;
+                continue;
+            }
+            r = response->covariance_matrix[i * n + j] /
+                (response->volatilities[i] * response->volatilities[j]);
+            /* Rounding can put a perfect correlation a few ulps outside the
+             * range; the estimate itself is unchanged, only its presentation. */
+            if (r > 1.0) r = 1.0;
+            if (r < -1.0) r = -1.0;
+            response->correlation_matrix[i * n + j] = r;
+        }
+    }
+
+    /* --- Cholesky. A complete-case sample covariance is positive
+     * semi-definite by construction, so a failure here means the sample is
+     * rank-deficient: fewer complete observations than assets, or a duplicated
+     * series. Report it the way composeCovariance does and return no numbers. */
+    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) {
+            cfr_drop_outputs(env, response);
+            return cfr_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) {
+            int idx = failed_at;
+            cfr_drop_outputs(env, response);
+            return cfr_fail(env, response,
+                "Not positive definite: Cholesky failed at index %d. A complete-case "
+                "sample covariance is positive semi-definite, so this is a rank-deficient "
+                "sample: %d complete observations for %d assets (%d or more are needed), "
+                "or two series that move together exactly.",
+                idx, used, n, n + 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: covarianceFromReturns for %d assets over %d of %d observations "
+        "in %ld us (positive_definite=%s)",
+        n, used, obs, response->calc_time_us,
+        response->positive_definite_checked
+            ? (response->positive_definite ? "true" : "false") : "unchecked");
+
+    return response;
+}
+
+AXIS2_EXTERN axis2_char_t* AXIS2_CALL
+finbench_covariance_from_returns_response_to_json(
+    const finbench_covariance_from_returns_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));
+    json_object_object_add(json_resp, "n_obs_provided",
+        json_object_new_int(response->n_obs_provided));
+    json_object_object_add(json_resp, "n_obs_used",
+        json_object_new_int(response->n_obs_used));
+    json_object_object_add(json_resp, "n_periods_per_year",
+        json_object_new_double(response->n_periods_per_year));
+
+    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);
+    }
+    if (response->mean_returns) {
+        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->mean_returns[i]));
+        json_object_object_add(json_resp, "mean_returns", arr);
+    }
+    if (response->observations_per_pair) {
+        json_object *arr = json_object_new_array();
+        for (i = 0; i < (int)matrix_size; i++)
+            json_object_array_add(arr, json_object_new_int(response->observations_per_pair[i]));
+        json_object_object_add(json_resp, "observations_per_pair", 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 covariance from returns
+ */
+AXIS2_EXTERN axis2_char_t* AXIS2_CALL
+finbench_covariance_from_returns_json_only(
+    const axutil_env_t *env,
+    const axis2_char_t *json_request)
+{
+    finbench_covariance_from_returns_request_t *request;
+    finbench_covariance_from_returns_response_t *response;
+    axis2_char_t *json_response;
+
+    request = finbench_covariance_from_returns_request_create_from_json(env, json_request);
+    if (!request) {
+        return axutil_strdup(env,
+            "{\"status\":\"FAILED\",\"error_message\":"
+            "\"Failed to parse covarianceFromReturns request. "
+            "Required field: returns, either n_assets arrays of n_obs numbers or one "
+            "flat row-major array with n_assets and n_obs given. "
+            "n_assets must be in [1, 50] and n_obs in [2, 5000]; a null element means "
+            "the observation is missing. "
+            "Optional: n_periods_per_year (float > 0, default 252), "
+            "check_positive_definite (bool, default true), asset_ids (string[]), "
+            "request_id (string).\"}");
+    }
+
+    response = finbench_covariance_from_returns(env, request);
+    json_response = finbench_covariance_from_returns_response_to_json(response, env);
+
+    finbench_covariance_from_returns_request_free(request, env);
+    finbench_covariance_from_returns_response_free(response, env);
+
+    return json_response;
+}
+
 /* ============================================================================
  * Monte Carlo Simulation Implementation
  * ============================================================================
@@ -3128,6 +3819,7 @@ finbench_get_metadata_json(const axutil_env_t *env)
     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("covarianceFromReturns"));
     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"));
@@ -3320,6 +4012,11 @@ finbench_dispatch_json_obj(
         {
             result_str = finbench_compose_covariance_json_only(env, json_str);
         }
+        else if (strcmp(action, "covarianceFromReturns") == 0 ||
+                 strcmp(action, "covariance_from_returns") == 0)
+        {
+            result_str = finbench_covariance_from_returns_json_only(env, json_str);
+        }
         else if (strcmp(action, "monteCarlo") == 0 ||
                  strcmp(action, "monte_carlo") == 0)
         {
@@ -3371,16 +4068,25 @@ finbench_dispatch_json_obj(
          * MCP dispatches by tool name; only a direct caller can hit this. */
         json_object *temp_obj;
 
+        /* "returns" is covarianceFromReturns' alone: it is the only operation
+         * that takes a sample, and monteCarlo's nearest key is the distinct
+         * "expected_returns". Tested first because it is unambiguous. */
+        if (json_object_object_get_ex(json_request, "returns", &temp_obj))
+        {
+            AXIS2_LOG_INFO(env->log,
+                "FinancialBenchmarkService: Detected covarianceFromReturns request");
+            result_str = finbench_covariance_from_returns_json_only(env, json_str);
+        }
         /* Any simulation parameter means Monte Carlo */
-        if (json_object_object_get_ex(json_request, "n_simulations", &temp_obj) ||
-            json_object_object_get_ex(json_request, "initial_value", &temp_obj) ||
-            json_object_object_get_ex(json_request, "expected_return", &temp_obj) ||
-            json_object_object_get_ex(json_request, "expected_returns", &temp_obj) ||
-            json_object_object_get_ex(json_request, "volatility", &temp_obj) ||
-            json_object_object_get_ex(json_request, "random_seed", &temp_obj) ||
-            json_object_object_get_ex(json_request, "model", &temp_obj) ||
-            json_object_object_get_ex(json_request, "n_periods", &temp_obj) ||
-            json_object_object_get_ex(json_request, "percentiles", &temp_obj))
+        else if (json_object_object_get_ex(json_request, "n_simulations", &temp_obj) ||
+                 json_object_object_get_ex(json_request, "initial_value", &temp_obj) ||
+                 json_object_object_get_ex(json_request, "expected_return", &temp_obj) ||
+                 json_object_object_get_ex(json_request, "expected_returns", &temp_obj) ||
+                 json_object_object_get_ex(json_request, "volatility", &temp_obj) ||
+                 json_object_object_get_ex(json_request, "random_seed", &temp_obj) ||
+                 json_object_object_get_ex(json_request, "model", &temp_obj) ||
+                 json_object_object_get_ex(json_request, "n_periods", &temp_obj) ||
+                 json_object_object_get_ex(json_request, "percentiles", &temp_obj))
         /* n_periods_per_year is deliberately not a marker: portfolioVariance
          * takes it too, and it must keep routing to the variance operation. */
         {
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 6e9e24690..3436180a5 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
@@ -76,6 +76,22 @@ extern "C"
  * response amplification. 500 assets: ~4e7 Cholesky steps, ~10 MB reply. */
 #define FINBENCH_MAX_COV_ASSETS     500
 
+/** Maximum assets in a covarianceFromReturns request. Much smaller than
+ * FINBENCH_MAX_COV_ASSETS: the input is O(n_assets x n_obs) numbers rather
+ * than a handful, so 50 names x 5000 observations is already 250,000 doubles
+ * (~4 MB of JSON), and the estimator itself is O(n_assets^2 x n_obs). A book
+ * wider than this belongs in a file the service reads, not in a request. */
+#define FINBENCH_MAX_RET_ASSETS     50
+
+/** Maximum observations per asset in a covarianceFromReturns request.
+ * 5,000 daily returns is about twenty years. */
+#define FINBENCH_MAX_OBS            5000
+
+/** Default periodisation for covarianceFromReturns: US equity trading days.
+ * Daily variance scales linearly with the horizon under i.i.d. returns, so
+ * the annual covariance is the per-period covariance times this factor. */
+#define FINBENCH_DEFAULT_PERIODS_PER_YEAR   252.0
+
 /** Maximum assets in a correlated (multi-asset) monteCarlo request. The work
  * cap for a book is n_simulations × n_periods × n_assets ≤ FINBENCH_MAX_WORK:
  * one asset-step (a normal draw and an exp()) is the unit, so the defaults
@@ -387,6 +403,142 @@ typedef struct finbench_compose_covariance_response
 
 } finbench_compose_covariance_response_t;
 
+/**
+ * @brief Covariance From Returns Request
+ *
+ * Sample covariance of a return matrix: the estimator the JavaScript in the
+ * demo app runs on its CSV, moved into C so the same numbers can be produced
+ * without a browser. Pearson (1896); Bessel's (n-1) correction.
+ *
+ * Missing data is complete-case: an observation where any asset is missing is
+ * dropped for every asset, so the matrix comes from one common sample and is
+ * positive semi-definite by construction. observations_per_pair in the
+ * response reports what a pairwise-complete estimator would have had to work
+ * with, so a caller can see when dropping was expensive. A production
+ * estimator would add pairwise completion and shrinkage; this one does not.
+ */
+typedef struct finbench_covariance_from_returns_request
+{
+    /** Number of assets (rows); inferred from a 2D returns array. */
+    int n_assets;
+
+    /** Number of observations per asset (columns); inferred from the first row. */
+    int n_obs;
+
+    /**
+     * Returns, row-major: asset i observation t at i*n_obs + t. A JSON null
+     * is stored as NaN and means "not observed"; every other element must be
+     * a number. Periodic returns (log or simple, the caller's choice), not
+     * prices and not percentages.
+     */
+    double *returns;
+
+    /** Outer array length as parsed, for dimension validation. */
+    int rows_provided;
+
+    /** Row length used (first row, or the flat element count). */
+    int obs_provided;
+
+    /** First row whose length differs from the first row's, else -1. */
+    int ragged_at;
+
+    /** Position of the first element that is neither a number nor null, else -1. */
+    int bad_value_row;
+    int bad_value_col;
+
+    /** AXIS2_TRUE when returns arrived flat (n_assets*n_obs) rather than 2D. */
+    axis2_bool_t flat_shape;
+
+    /**
+     * Periods per year used to annualise: the covariance is multiplied by it
+     * and the mean returns with it. Must be finite and > 0. Pass 1 to leave
+     * the result in the input's own basis. Default
+     * FINBENCH_DEFAULT_PERIODS_PER_YEAR.
+     */
+    double n_periods_per_year;
+
+    /** 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_covariance_from_returns_request_t;
+
+/**
+ * @brief Covariance From Returns Response
+ */
+typedef struct finbench_covariance_from_returns_response
+{
+    /** Processing status */
+    char *status;
+
+    /** Number of assets */
+    int n_assets;
+
+    /** Observations supplied per asset */
+    int n_obs_provided;
+
+    /** Complete observations the estimator actually used */
+    int n_obs_used;
+
+    /** Annualisation factor as applied */
+    double n_periods_per_year;
+
+    /** Sigma, flattened row-major, n_assets^2 elements. NULL on FAILED. */
+    double *covariance_matrix;
+
+    /** Correlation matrix implied by Sigma, flattened row-major. NULL on FAILED. */
+    double *correlation_matrix;
+
+    /** Per-asset volatilities, sqrt of the diagonal. NULL on FAILED. */
+    double *volatilities;
+
+    /** Per-asset mean return, annualised with the same factor. NULL on FAILED. */
+    double *mean_returns;
+
+    /**
+     * Observations where both assets of the pair are present, flattened
+     * row-major. Equal to n_obs_used everywhere when nothing is missing; a
+     * pair above it lost data to another asset's gaps. NULL on FAILED.
+     */
+    int *observations_per_pair;
+
+    /** 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^2 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_covariance_from_returns_response_t;
+
 /* ============================================================================
  * Monte Carlo Simulation
  *
@@ -1072,6 +1224,65 @@ finbench_compose_covariance_json_only(
     const axutil_env_t *env,
     const axis2_char_t *json_request);
 
+/* ============================================================================
+ * Function Declarations - Covariance From Returns
+ * ============================================================================
+ */
+
+/**
+ * @brief Create covariance-from-returns request from JSON
+ */
+AXIS2_EXTERN finbench_covariance_from_returns_request_t* AXIS2_CALL
+finbench_covariance_from_returns_request_create_from_json(
+    const axutil_env_t *env,
+    const axis2_char_t *json_string);
+
+/**
+ * @brief Free covariance-from-returns request
+ */
+AXIS2_EXTERN void AXIS2_CALL
+finbench_covariance_from_returns_request_free(
+    finbench_covariance_from_returns_request_t *request,
+    const axutil_env_t *env);
+
+/**
+ * @brief Create covariance-from-returns response
+ */
+AXIS2_EXTERN finbench_covariance_from_returns_response_t* AXIS2_CALL
+finbench_covariance_from_returns_response_create(const axutil_env_t *env);
+
+/**
+ * @brief Convert covariance-from-returns response to JSON
+ */
+AXIS2_EXTERN axis2_char_t* AXIS2_CALL
+finbench_covariance_from_returns_response_to_json(
+    const finbench_covariance_from_returns_response_t *response,
+    const axutil_env_t *env);
+
+/**
+ * @brief Free covariance-from-returns response
+ */
+AXIS2_EXTERN void AXIS2_CALL
+finbench_covariance_from_returns_response_free(
+    finbench_covariance_from_returns_response_t *response,
+    const axutil_env_t *env);
+
+/**
+ * @brief Sample covariance of a return matrix (main operation)
+ */
+AXIS2_EXTERN finbench_covariance_from_returns_response_t* AXIS2_CALL
+finbench_covariance_from_returns(
+    const axutil_env_t *env,
+    finbench_covariance_from_returns_request_t *request);
+
+/**
+ * @brief Process covariance-from-returns with pure JSON (HTTP/2 endpoint)
+ */
+AXIS2_EXTERN axis2_char_t* AXIS2_CALL
+finbench_covariance_from_returns_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 6576908b1..f8d5704b4 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
@@ -469,6 +469,12 @@ route_operation(
         result = finbench_compose_covariance_json_only(env, json_request);
     }
 
+    /* covarianceFromReturns operation */
+    else if (axutil_strcmp(operation_name, "covarianceFromReturns") == 0)
+    {
+        result = finbench_covariance_from_returns_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 fbdf2c644..6015203c9 100644
--- a/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
+++ b/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
@@ -113,6 +113,35 @@ static const char SCHEMA_COMPOSE_COVARIANCE[] =
     "\"required\":[\"volatilities\"]"
     "}";
 
+static const char SCHEMA_COVARIANCE_FROM_RETURNS[] =
+    "{"
+    "\"type\":\"object\","
+    "\"properties\":{"
+        "\"returns\":{\"type\":\"array\","
+            "\"description\":\"Periodic returns, one array of n_obs numbers per asset "
+                "(or one flat row-major array with n_assets and n_obs given). Log or simple "
+                "returns, not prices and not percentages. A null element means that "
+                "observation is missing. Max 50 assets x 5000 observations\"},"
+        "\"n_assets\":{\"type\":\"integer\","
+            "\"description\":\"Number of assets (max 50). Inferred from a nested returns array\"},"
+        "\"n_obs\":{\"type\":\"integer\","
+            "\"description\":\"Observations per asset (max 5000). Inferred from the first row\"},"
+        "\"n_periods_per_year\":{\"type\":\"number\","
+            "\"description\":\"Annualization factor: the covariance is multiplied by it and the "
+                "means with it. 252 for daily returns (the default), 12 for monthly, 1 to keep "
+                "the input's own basis. The result is what portfolioVariance wants with "
+                "n_periods_per_year=1\"},"
+        "\"check_positive_definite\":{\"type\":\"boolean\","
+            "\"description\":\"Run a Cholesky decomposition and refuse a rank-deficient sample. "
+                "Default true\"},"
+        "\"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\":[\"returns\"]"
+    "}";
+
 static const char SCHEMA_MONTE_CARLO[] =
     "{"
     "\"type\":\"object\","
@@ -235,6 +264,18 @@ static const finbench_mcp_tool_t finbench_mcp_tools[] = {
         "correlations are short, bounded and checkable; a raw matrix is not.",
         SCHEMA_COMPOSE_COVARIANCE
     },
+    {
+        "covarianceFromReturns",
+        "Sample covariance matrix from a return history: Sigma_ij = sum_t (r_it - mean_i)(r_jt - mean_j) "
+        "/ (m - 1), annualized by n_periods_per_year. Returns the flat row-major covariance_matrix that "
+        "portfolioVariance and monteCarlo accept, the implied correlation matrix, per-asset volatilities "
+        "and annualized mean returns, and observations_per_pair. Missing data is complete-case: an "
+        "observation where any asset is null is dropped for every asset, so one common sample backs every "
+        "entry; observations_per_pair shows what each pair would have had otherwise. Runs the same Cholesky "
+        "check as composeCovariance. This is the textbook estimator, with no shrinkage and no pairwise "
+        "completion: for a history with real gaps, prefer a library that has both.",
+        SCHEMA_COVARIANCE_FROM_RETURNS
+    },
     {
         "monteCarlo",
         "Monte Carlo Value at Risk (VaR) simulation using Geometric Brownian Motion: "
@@ -439,6 +480,8 @@ static json_object *mcp_handle_tools_call(
         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, "covarianceFromReturns") == 0) {
+        result_json = finbench_covariance_from_returns_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) {
@@ -446,7 +489,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, composeCovariance, monteCarlo, scenarioAnalysis";
+        *out_msg  = "Unknown tool name. Available: portfolioVariance, composeCovariance, covarianceFromReturns, 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 f7af5ae32..75b2b41a8 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,9 @@
  * @file finbench_mcp.h
  * @brief MCP stdio transport for the Financial Benchmark Service (C1)
  *
- * Implements JSON-RPC 2.0 over stdin/stdout for the four financial benchmark
- * operations: portfolioVariance, composeCovariance, monteCarlo, scenarioAnalysis.
+ * Implements JSON-RPC 2.0 over stdin/stdout for the five financial benchmark
+ * operations: portfolioVariance, composeCovariance, covarianceFromReturns,
+ * 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
@@ -51,6 +52,7 @@
  * Tools exposed:
  *   - portfolioVariance  (O(n^2) covariance matrix multiplication)
  *   - composeCovariance  (Sigma = D*R*D from vols + correlations, Cholesky-checked)
+ *   - covarianceFromReturns (sample Sigma from a return history, complete-case)
  *   - monteCarlo         (GBM Value-at-Risk simulation)
  *   - scenarioAnalysis   (expected return + HashMap vs ArrayList benchmark)
  */


(axis-axis2-c-core) 04/07: Refuse asymmetric and non-PSD matrices in portfolioVariance

Posted by ro...@apache.org.
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 22274a4218c16eb38730829a7397c752f7deee96
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Sun Sep 20 20:15:42 2026 -1000

    Refuse asymmetric and non-PSD matrices in portfolioVariance
    
    Symmetry is now checked with the same tolerance monteCarlo and
    composeCovariance use, and a w'Sigma*w that comes out negative beyond
    rounding scale is refused instead of being clamped to a zero volatility.
    The in-function fallback for n_periods_per_year is 1, matching the parser
    default set earlier, so a zeroed struct never reverts to 252.
    
    Co-Authored-By: Claude Fable 5.1 <no...@anthropic.com>
---
 .../src/financial_benchmark_service.c              | 73 +++++++++++++++++++---
 .../src/financial_benchmark_service.h              | 10 ++-
 2 files changed, 72 insertions(+), 11 deletions(-)

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 564e3b9d8..322ffd9b3 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
@@ -557,6 +557,35 @@ finbench_calculate_portfolio_variance(
         }
     }
 
+    /* Symmetry, same rule and tolerance as monteCarlo and composeCovariance.
+     * w'Σw over an asymmetric matrix silently uses the mean of each
+     * off-diagonal pair, so the caller gets a number that agrees with no
+     * symmetrized version of what they sent. Refuse with the index. */
+    for (i = 0; i < n; i++) {
+        for (j = 0; j < i; j++) {
+            double a = request->covariance_matrix[i * n + j];
+            double b = request->covariance_matrix[j * n + i];
+            if (!isfinite(a) || !isfinite(b)) {
+                char err_buf[160];
+                snprintf(err_buf, sizeof(err_buf),
+                    "covariance_matrix[%d][%d] or [%d][%d] is not finite.", i, j, j, i);
+                response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+                response->error_message = axutil_strdup(env, err_buf);
+                AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI, "FinBench portfolioVariance: %s", err_buf);
+                return response;
+            }
+            if (fabs(a - b) > FINBENCH_CORR_TOL * (1.0 + fabs(a))) {
+                char err_buf[200];
+                snprintf(err_buf, sizeof(err_buf),
+                    "covariance_matrix is not symmetric at (%d,%d): %g vs %g.", i, j, a, b);
+                response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+                response->error_message = axutil_strdup(env, err_buf);
+                AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI, "FinBench portfolioVariance: %s", err_buf);
+                return response;
+            }
+        }
+    }
+
     start_time = get_time_us();
 
     /*
@@ -565,26 +594,54 @@ finbench_calculate_portfolio_variance(
      *
      * This is common for correlation calculations.
      * On a 500-asset portfolio, this is 250,000 operations.
+     *
+     * `magnitude` accumulates the absolute terms so a negative result can be
+     * judged against the size of what was summed: cancellation noise is a
+     * negative of order 1e-16 × magnitude; a matrix that is not positive
+     * semi-definite for these weights is a negative of order magnitude.
      */
+    double magnitude = 0.0;
     for (i = 0; i < n; i++) {
         for (j = 0; j < n; j++) {
-            variance += request->weights[i] *
-                        request->weights[j] *
-                        request->covariance_matrix[i * n + j];
+            double term = request->weights[i] *
+                          request->weights[j] *
+                          request->covariance_matrix[i * n + j];
+            variance += term;
+            magnitude += fabs(term);
             ops++;
         }
     }
 
     end_time = get_time_us();
 
-    int npy = (request->n_periods_per_year > 0) ? request->n_periods_per_year : 252;
+    /* The parser defaults this to 1 (annualized matrix); a direct C caller
+     * that left the struct zeroed gets the same default, never the old 252. */
+    int npy = (request->n_periods_per_year > 0) ? request->n_periods_per_year : 1;
 
     /*
-     * Clamp variance to zero before sqrt. Floating-point cancellation in the
-     * O(n²) sum can yield a tiny negative result (e.g., -1e-17) for a
-     * numerically near-zero portfolio variance, producing NaN from sqrt().
+     * A negative w'Σw means the matrix is not positive semi-definite for
+     * these weights — it is not a covariance matrix. Only a rounding-scale
+     * negative (cancellation in the sum) is clamped to zero; anything larger
+     * is refused, because a silently clamped zero volatility is a confidently
+     * wrong answer nothing downstream can detect. composeCovariance and
+     * covarianceFromReturns Cholesky-check what they produce; a matrix
+     * assembled by hand gets no such check anywhere else.
      */
-    if (variance < 0.0) variance = 0.0;
+    if (variance < 0.0) {
+        if (variance < -1e-9 * magnitude) {
+            char err_buf[240];
+            snprintf(err_buf, sizeof(err_buf),
+                "w'Sigma*w = %g is negative: covariance_matrix is not positive "
+                "semi-definite for these weights, so it is not a covariance matrix. "
+                "Build it with composeCovariance or covarianceFromReturns, which check.",
+                variance);
+            response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+            response->error_message = axutil_strdup(env, err_buf);
+            AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI, "FinBench portfolioVariance: %s", err_buf);
+            return response;
+        }
+        variance = 0.0;
+    }
     double volatility = sqrt(variance);
 
     /* Populate response */
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 99e066824..61debf83e 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
@@ -188,9 +188,13 @@ typedef struct finbench_portfolio_variance_request
      * annualized matrix (the default, n_periods_per_year = 1; what
      * composeCovariance and covarianceFromReturns return), or a per-period
      * matrix with n_periods_per_year = 252 (daily), 52 (weekly), 12 (monthly).
-     * A real covariance matrix is symmetric and positive-semi-definite;
-     * the implementation does not enforce symmetry, so cov[i][j] != cov[j][i]
-     * silently produces a different w'Σw than a symmetrized version.
+     * A real covariance matrix is symmetric and positive-semi-definite.
+     * Symmetry is enforced (within FINBENCH_CORR_TOL, as monteCarlo and
+     * composeCovariance enforce it) and an asymmetric matrix is refused
+     * naming the index. Positive semi-definiteness is not checked in full
+     * (that is what composeCovariance and covarianceFromReturns do when
+     * they build a matrix), but a w'Σw that comes out negative beyond
+     * rounding scale is refused rather than clamped to a zero volatility.
      */
     double *covariance_matrix;
 


(axis-axis2-c-core) 06/07: Check the diagonal for finiteness in portfolioVariance

Posted by ro...@apache.org.
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 1393ea33c74a9f6cc7ee1c338df4ddb47d2c36b8
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Tue Sep 22 13:26:53 2026 -1000

    Check the diagonal for finiteness in portfolioVariance
    
    The symmetry loop skipped i == j, so a NaN or -Inf on the diagonal
    bypassed the PSD guard and returned SUCCESS. The loop now includes the
    diagonal, and a non-finite w'Sigma*w (NaN weight, overflow) is refused.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
---
 .../src/financial_benchmark_service.c              | 28 ++++++++++++++++++----
 1 file changed, 24 insertions(+), 4 deletions(-)

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 322ffd9b3..c79329c41 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
@@ -560,15 +560,22 @@ finbench_calculate_portfolio_variance(
     /* Symmetry, same rule and tolerance as monteCarlo and composeCovariance.
      * w'Σw over an asymmetric matrix silently uses the mean of each
      * off-diagonal pair, so the caller gets a number that agrees with no
-     * symmetrized version of what they sent. Refuse with the index. */
+     * symmetrized version of what they sent. Refuse with the index.
+     * j <= i so the diagonal is finiteness-checked too: a NaN or -Inf
+     * there would otherwise slip past the PSD guard below (comparisons
+     * against NaN, and -Inf < -Inf, are all false). */
     for (i = 0; i < n; i++) {
-        for (j = 0; j < i; j++) {
+        for (j = 0; j <= i; j++) {
             double a = request->covariance_matrix[i * n + j];
             double b = request->covariance_matrix[j * n + i];
             if (!isfinite(a) || !isfinite(b)) {
                 char err_buf[160];
-                snprintf(err_buf, sizeof(err_buf),
-                    "covariance_matrix[%d][%d] or [%d][%d] is not finite.", i, j, j, i);
+                if (i == j)
+                    snprintf(err_buf, sizeof(err_buf),
+                        "covariance_matrix[%d][%d] is not finite.", i, i);
+                else
+                    snprintf(err_buf, sizeof(err_buf),
+                        "covariance_matrix[%d][%d] or [%d][%d] is not finite.", i, j, j, i);
                 response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
                 response->error_message = axutil_strdup(env, err_buf);
                 AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI, "FinBench portfolioVariance: %s", err_buf);
@@ -618,6 +625,19 @@ finbench_calculate_portfolio_variance(
      * that left the struct zeroed gets the same default, never the old 252. */
     int npy = (request->n_periods_per_year > 0) ? request->n_periods_per_year : 1;
 
+    /* Entries are finite by now, but a NaN weight or entries large enough
+     * to overflow the sum still give a non-finite result; refuse it rather
+     * than let it past the sign test below. */
+    if (!isfinite(variance)) {
+        response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+        response->error_message = axutil_strdup(env,
+            "w'Sigma*w is not finite: check weights and covariance_matrix "
+            "for non-finite or overflowing values.");
+        AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI,
+            "FinBench portfolioVariance: w'Sigma*w is not finite");
+        return response;
+    }
+
     /*
      * A negative w'Σw means the matrix is not positive semi-definite for
      * these weights — it is not a covariance matrix. Only a rounding-scale


(axis-axis2-c-core) 03/07: Default portfolioVariance to an annualized matrix and echo the basis

Posted by ro...@apache.org.
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 d5864fd66fcd42884ae85a56a9f4eed7ddead473
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Sun Sep 20 20:02:41 2026 -1000

    Default portfolioVariance to an annualized matrix and echo the basis
    
    n_periods_per_year now defaults to 1, the basis composeCovariance,
    covarianceFromReturns and every documented example produce; a caller with
    a per-period matrix passes 252, 52 or 12. The response echoes
    n_periods_per_year and covariance_basis, and both producers stamp
    covariance_basis "annualized". With the old 252 default an annualized
    matrix reported annualized_volatility sqrt(252) too large, undetectably.
    
    Co-Authored-By: Claude Fable 5.1 <no...@anthropic.com>
---
 docs/MCP_EXAMPLES.md                               | 14 ++++--
 .../financial-benchmark-service/README.md          | 14 ++++--
 .../financial-benchmark-service/services.xml       |  8 +++-
 .../src/financial_benchmark_service.c              | 55 +++++++++++++++++-----
 .../src/financial_benchmark_service.h              | 37 +++++++++------
 .../financial-benchmark-service/src/finbench_mcp.c | 17 ++++---
 6 files changed, 101 insertions(+), 44 deletions(-)

diff --git a/docs/MCP_EXAMPLES.md b/docs/MCP_EXAMPLES.md
index 80ba6ed72..78d794ccd 100644
--- a/docs/MCP_EXAMPLES.md
+++ b/docs/MCP_EXAMPLES.md
@@ -145,12 +145,16 @@ curl -k --http2 -s \
 ```
 
 > **Time basis.** Every covariance matrix in this document is already
-> annualized, so each request passes `"n_periods_per_year": 1` and
-> `annualized_volatility` equals `portfolio_volatility`. The service computes
+> annualized, which is also `portfolioVariance`'s default assumption
+> (`n_periods_per_year` defaults to 1), so `annualized_volatility` equals
+> `portfolio_volatility` and the requests below that still pass
+> `"n_periods_per_year": 1` are merely explicit. The service computes
 > `annualized_volatility = portfolio_volatility * sqrt(n_periods_per_year)`,
-> which is what you want for a *per-period* matrix — daily with 252, weekly
-> with 52, monthly with 12. Leaving the 252 default on an already-annualized
-> matrix multiplies the reported figure by sqrt(252) ~ 15.9. The service
+> so a *per-period* matrix needs the value stated — daily 252, weekly 52,
+> monthly 12 — and the response echoes `covariance_basis` ("annualized" or
+> "per_period") so what was applied is visible. Before 2026-09 the default was
+> 252, which on an annualized matrix multiplied the figure by sqrt(252) ~ 15.9
+> with nothing to flag it. The service
 > cannot detect the mismatch: a daily and an annualized covariance matrix are
 > both valid PSD matrices, and only the caller knows which was supplied.
 
diff --git a/samples/user_guide/financial-benchmark-service/README.md b/samples/user_guide/financial-benchmark-service/README.md
index c9826e4e1..4825f5d7d 100644
--- a/samples/user_guide/financial-benchmark-service/README.md
+++ b/samples/user_guide/financial-benchmark-service/README.md
@@ -137,8 +137,9 @@ 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`.
+time basis: annualized vols in, annualized Σ out (stamped
+`covariance_basis: "annualized"`), which is the basis `portfolioVariance`
+assumes by default.
 
 **Example Request** (uniform correlation, the stress case):
 ```json
@@ -167,7 +168,7 @@ time basis: annualized vols in, annualized Σ out, to be passed to
 ```
 
 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
+`[0.25, 0.25, 0.20, 0.15, 0.15]` gives a portfolio
 volatility of 0.2228; the same five vols with the historical correlation matrix
 give 0.1567. Feeding it to `monteCarlo` with `weights` runs the correlated
 book (see operation 2).
@@ -233,8 +234,11 @@ why they read as 63% and 45%: the factor multiplies whatever sample it is
 given, and a five-day sample is not a year.
 
 Pass `n_periods_per_year: 1` to keep the input's own basis; pass 12 for
-monthly data. The annualised matrix goes to `portfolioVariance` with
-`n_periods_per_year: 1`, since it is already annual.
+monthly data. The annualised matrix goes to `portfolioVariance` as it is:
+that operation takes its matrix as annualised by default (its own
+`n_periods_per_year` defaults to 1, and is only for callers holding a
+per-period matrix), and it echoes `covariance_basis` so the basis applied is
+visible in the response.
 
 **Refusals** name the field: a ragged `returns` row, an element that is neither
 a number nor `null`, a non-finite value, fewer than two complete observations,
diff --git a/samples/user_guide/financial-benchmark-service/services.xml b/samples/user_guide/financial-benchmark-service/services.xml
index 1bb77f5f1..a4536fcb1 100644
--- a/samples/user_guide/financial-benchmark-service/services.xml
+++ b/samples/user_guide/financial-benchmark-service/services.xml
@@ -81,7 +81,7 @@
              Target: <10ms on old Android phone
              ================================================================ -->
         <operation name="portfolioVariance">
-            <parameter name="mcpDescription">Calculate portfolio variance using an O(n²) covariance matrix. Returns portfolio volatility and annualised volatility. Benchmark: 500 assets in &lt;10ms on constrained hardware.</parameter>
+            <parameter name="mcpDescription">Calculate portfolio variance using an O(n²) covariance matrix. The matrix is taken as annualised by default (n_periods_per_year 1), which is what composeCovariance and covarianceFromReturns return; pass 252 only for a daily matrix. Returns portfolio volatility, annualised volatility and the basis applied. Benchmark: 500 assets in &lt;10ms on constrained hardware.</parameter>
             <parameter name="mcpReadOnly">true</parameter>
             <parameter name="mcpIdempotent">true</parameter>
             <parameter name="mcpInputSchema">{
@@ -96,6 +96,8 @@
                                           "description": "Exactly n_assets×n_assets values, row-major order (element [i*n+j] is cov(i,j))"},
                     "normalize_weights": {"type": "boolean",  "default": false,
                                           "description": "Scale weights to sum=1 before calculation. Default: false — caller must supply weights that already sum to 1."},
+                    "n_periods_per_year": {"type": "integer", "default": 1,
+                                          "description": "Periods per year of the INPUT matrix. Default 1 = already annualised (any matrix from composeCovariance or covarianceFromReturns). Only for a per-period matrix: 252 daily, 52 weekly, 12 monthly."},
                     "request_id":        {"type": "string",   "description": "Optional correlation ID echoed in the response"}
                 }
             }</parameter>
@@ -120,7 +122,9 @@
                     "status": "SUCCESS",
                     "portfolio_variance": 0.0156,
                     "portfolio_volatility": 0.125,
-                    "annualized_volatility": 1.98,
+                    "annualized_volatility": 0.125,
+                    "n_periods_per_year": 1,
+                    "covariance_basis": "annualized",
                     "calc_time_us": 4523,
                     "memory_used_kb": 28456,
                     "matrix_operations": 250000,
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 862b05f24..564e3b9d8 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
@@ -323,12 +323,19 @@ finbench_portfolio_variance_request_create_from_json(
         request->normalize_weights = AXIS2_FALSE;
     }
 
-    /* n_periods_per_year — default 252 (equity trading days) */
+    /* n_periods_per_year — default 1: the matrix is taken as already
+     * annualized, which is what composeCovariance, covarianceFromReturns and
+     * every documented example produce. A caller holding a per-period matrix
+     * (daily, weekly, monthly) passes 252, 52 or 12 explicitly. The default
+     * was 252 until 2026-09; on an annualized matrix that reported an
+     * annualized_volatility sqrt(252) ~ 15.9x too large, and the service has
+     * no way to detect the mismatch, so the default now matches the basis its
+     * own producers emit. */
     if (json_object_object_get_ex(json_obj, "n_periods_per_year", &value_obj)) {
         int npy = json_object_get_int(value_obj);
-        request->n_periods_per_year = (npy > 0) ? npy : 252;
+        request->n_periods_per_year = (npy > 0) ? npy : 1;
     } else {
-        request->n_periods_per_year = 252;
+        request->n_periods_per_year = 1;
     }
 
     json_object_put(json_obj);
@@ -401,14 +408,15 @@ finbench_portfolio_variance_response_free(
  * Formula: σ²_p = Σ_i Σ_j w_i * w_j * σ_ij
  *
  * The output basis matches the input basis — if the caller passes a
- * daily covariance matrix, σ²_p is a daily variance, and the caller
- * can annualize by sqrt(252) externally. The service additionally
- * emits an annualized_volatility field computed as
+ * daily covariance matrix, σ²_p is a daily variance. The service
+ * additionally emits an annualized_volatility field computed as
  *   portfolio_volatility * sqrt(n_periods_per_year)
- * which is correct for a PER-PERIOD input matrix. Callers who pass a
- * pre-annualized matrix (common in quant practice) should pass
- * n_periods_per_year=1 so the field equals portfolio_volatility;
- * leaving the 252 default double-annualizes it by sqrt(252) ~ 15.9.
+ * with n_periods_per_year defaulting to 1, i.e. the matrix is taken as
+ * already annualized — the basis composeCovariance and
+ * covarianceFromReturns emit. A caller with a PER-PERIOD matrix passes
+ * 252 (daily), 52 (weekly) or 12 (monthly) explicitly. The response
+ * echoes n_periods_per_year and a covariance_basis of "annualized" or
+ * "per_period" so the basis applied is always visible to the caller.
  *
  * Weight normalization edge case:
  *   When normalize_weights=true, weights are rescaled in-place so they
@@ -584,6 +592,7 @@ finbench_calculate_portfolio_variance(
     response->portfolio_variance = variance;
     response->portfolio_volatility = volatility;
     response->annualized_volatility = volatility * sqrt((double)npy);
+    response->n_periods_per_year = npy;
     response->calc_time_us = end_time - start_time;
     response->memory_used_kb = finbench_get_memory_usage_kb();
     response->matrix_operations = ops;
@@ -631,6 +640,17 @@ finbench_portfolio_variance_response_to_json(
     json_object_object_add(json_resp, "annualized_volatility",
         json_object_new_double(response->annualized_volatility));
 
+    /* Make the basis that was applied visible: a caller who sees
+     * covariance_basis "per_period" on a matrix they know is annual has
+     * their answer without reading the docs. Only meaningful on success. */
+    if (response->n_periods_per_year > 0) {
+        json_object_object_add(json_resp, "n_periods_per_year",
+            json_object_new_int(response->n_periods_per_year));
+        json_object_object_add(json_resp, "covariance_basis",
+            json_object_new_string(response->n_periods_per_year == 1
+                                   ? "annualized" : "per_period"));
+    }
+
     json_object_object_add(json_resp, "calc_time_us",
         json_object_new_int64(response->calc_time_us));
 
@@ -696,8 +716,8 @@ finbench_portfolio_variance_json_only(
             "\"Failed to parse portfolioVariance request. "
             "Required fields: n_assets (int), weights (float[]), "
             "covariance_matrix (float[n²] flat or float[n][n] 2D). "
-            "Optional: normalize_weights (bool), n_periods_per_year (int, default 252), "
-            "request_id (string).\"}");
+            "Optional: normalize_weights (bool), n_periods_per_year (int, default 1 = "
+            "matrix already annualized; 252 for a daily matrix), request_id (string).\"}");
     }
 
     response = finbench_calculate_portfolio_variance(env, request);
@@ -1229,6 +1249,12 @@ finbench_compose_covariance_response_to_json(
     json_object_object_add(json_resp, "n_assets",
         json_object_new_int(response->n_assets));
 
+    /* Σ = D·R·D inherits the vols' basis; annualized vols are the documented
+     * input, so the matrix is annualized. Stamped so a consumer can check the
+     * basis instead of assuming it. portfolioVariance defaults to this basis. */
+    json_object_object_add(json_resp, "covariance_basis",
+        json_object_new_string("annualized"));
+
     if (response->covariance_matrix) {
         json_object *arr = json_object_new_array();
         for (i = 0; i < (int)matrix_size; i++)
@@ -1905,6 +1931,11 @@ finbench_covariance_from_returns_response_to_json(
         json_object_new_int(response->n_obs_used));
     json_object_object_add(json_resp, "n_periods_per_year",
         json_object_new_double(response->n_periods_per_year));
+    /* The sample covariance was multiplied by n_periods_per_year, so the
+     * matrix is on an annual basis whatever the input frequency was. Stamped
+     * for consumers; portfolioVariance defaults to this basis. */
+    json_object_object_add(json_resp, "covariance_basis",
+        json_object_new_string("annualized"));
 
     if (response->covariance_matrix) {
         json_object *arr = json_object_new_array();
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 3436180a5..99e066824 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
@@ -142,14 +142,17 @@ extern "C"
  *
  * Time basis (important — frequency consistency):
  *   σ²_p is computed in whatever time basis the input covariance matrix
- *   uses. If the caller provides a DAILY covariance matrix, σ²_p is a
- *   daily variance — in which case "annualized volatility" is computed
- *   by multiplying sqrt(σ²_p) by sqrt(n_periods_per_year). If the caller
- *   already provides an ANNUALIZED matrix (as quants often do), they
- *   should pass n_periods_per_year=1, which makes annualized_volatility
- *   equal portfolio_volatility. Leaving the 252 default on an
- *   already-annualized matrix over-annualizes the reported figure by
- *   sqrt(252) ~ 15.9. The service cannot detect the mismatch: a daily
+ *   uses. The matrix is taken as ANNUALIZED by default
+ *   (n_periods_per_year = 1), which is the basis composeCovariance and
+ *   covarianceFromReturns emit and every documented example uses; then
+ *   annualized_volatility equals portfolio_volatility. A caller with a
+ *   PER-PERIOD matrix (daily, weekly, monthly) passes n_periods_per_year
+ *   = 252, 52 or 12, and annualized_volatility is sqrt(σ²_p) times
+ *   sqrt(n_periods_per_year). The response echoes n_periods_per_year and
+ *   covariance_basis ("annualized" | "per_period") so the basis applied is
+ *   visible. (Until 2026-09 the default was 252, which on an annualized
+ *   matrix over-reported by sqrt(252) ~ 15.9 with nothing to flag it.)
+ *   The service cannot detect the mismatch itself: a daily
  *   and an annualized covariance matrix are both valid PSD matrices,
  *   and only the caller knows which was supplied.
  *
@@ -181,9 +184,10 @@ typedef struct finbench_portfolio_variance_request
 
     /**
      * Covariance matrix, flattened row-major: element (i,j) is at index
-     * i * n_assets + j. The matrix MUST be on a per-period basis — e.g.,
-     * daily returns covariance when n_periods_per_year = 252, or annual
-     * returns covariance when n_periods_per_year = 1.
+     * i * n_assets + j. Its time basis must match n_periods_per_year: an
+     * annualized matrix (the default, n_periods_per_year = 1; what
+     * composeCovariance and covarianceFromReturns return), or a per-period
+     * matrix with n_periods_per_year = 252 (daily), 52 (weekly), 12 (monthly).
      * A real covariance matrix is symmetric and positive-semi-definite;
      * the implementation does not enforce symmetry, so cov[i][j] != cov[j][i]
      * silently produces a different w'Σw than a symmetrized version.
@@ -209,9 +213,10 @@ typedef struct finbench_portfolio_variance_request
     axis2_bool_t normalize_weights;
 
     /**
-     * Trading periods per year used for annualizing volatility.
-     * Default: 252 (equity trading days). Use 260 for some fixed-income conventions,
-     * 365 for crypto, or 12 for monthly data.
+     * Periods per year of the INPUT matrix, used to annualize volatility.
+     * Default: 1 — the matrix is already annualized (the basis the service's
+     * own producers emit). Pass 252 for a daily matrix (260 for some
+     * fixed-income conventions, 365 for crypto), 52 weekly, 12 monthly.
      */
     int n_periods_per_year;
 
@@ -234,6 +239,10 @@ typedef struct finbench_portfolio_variance_response
     /** Annualized volatility (σ × sqrt(n_periods_per_year)) */
     double annualized_volatility;
 
+    /** The n_periods_per_year that was applied (echoed; 1 = matrix taken as
+     * annualized). 0 on a failed response. Serialized with covariance_basis. */
+    int n_periods_per_year;
+
     /** Actual sum of weights as provided (before any normalization) */
     double weight_sum;
 
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 6dbadc866..215b74b89 100644
--- a/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
+++ b/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
@@ -72,12 +72,16 @@ static const char SCHEMA_PORTFOLIO_VARIANCE[] =
         "\"weights\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},"
             "\"description\":\"Portfolio weights. Must sum to 1.0 unless normalize_weights=true\"},"
         "\"covariance_matrix\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},"
-            "\"description\":\"Flattened n_assets x n_assets covariance matrix (row-major order)\"},"
+            "\"description\":\"Flattened n_assets x n_assets covariance matrix (row-major order). "
+                "Taken as ANNUALIZED unless n_periods_per_year says otherwise - which is what "
+                "composeCovariance, covarianceFromReturns and covarianceFromCsv return, so pass "
+                "their covariance_matrix straight through\"},"
         "\"normalize_weights\":{\"type\":\"boolean\","
             "\"description\":\"Rescale weights to sum to 1.0 before computing variance. Default: false\"},"
-        "\"n_periods_per_year\":{\"type\":\"integer\","
-            "\"description\":\"Trading periods per year for annualizing volatility. "
-                "Default 252 (equity). Use 260 (some fixed-income), 365 (crypto), 12 (monthly)\"},"
+        "\"n_periods_per_year\":{\"type\":\"integer\",\"default\":1,"
+            "\"description\":\"Periods per year of the INPUT matrix. Default 1 = the matrix is already "
+                "annualized (leave it for any matrix produced by this service). Only for a per-period "
+                "matrix: 252 daily, 52 weekly, 12 monthly. The response echoes covariance_basis\"},"
         "\"request_id\":{\"type\":\"string\","
             "\"description\":\"Optional identifier echoed in the response for request tracing\"}"
     "},"
@@ -242,8 +246,9 @@ static const finbench_mcp_tool_t finbench_mcp_tools[] = {
         "portfolioVariance",
         "Calculate portfolio variance using O(n^2) covariance matrix multiplication: "
         "sigma_p^2 = sum_i sum_j w_i * w_j * sigma_ij. "
-        "Returns variance, volatility, annualized volatility (sigma * sqrt(n_periods_per_year)), "
-        "matrix operation count, and microsecond timing. "
+        "Returns variance, volatility, annualized volatility (sigma * sqrt(n_periods_per_year); the "
+        "matrix is taken as annualized by default, so this equals volatility unless you pass a "
+        "per-period matrix), the basis applied, matrix operation count, and microsecond timing. "
         "Target: 500 assets in ~5ms on resource-constrained hardware.",
         SCHEMA_PORTFOLIO_VARIANCE
     },


(axis-axis2-c-core) 02/07: Let an embedding application add MCP tools to the sample transport

Posted by ro...@apache.org.
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 eca314fb69063b0f45980598a572b347d84cbeca
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Sun Sep 20 12:30:20 2026 -1000

    Let an embedding application add MCP tools to the sample transport
    
    tools/list and tools/call now consult finbench_mcp_extra_tools() and
    finbench_mcp_extra_dispatch(), both weak and empty by default, so a plain
    build is unchanged. Built-ins are tried first, so an extra tool cannot
    shadow an operation. The sample has no data of its own; an application
    that has some can keep the file reading and its path validation on its own
    side and still reach these operations through one transport.
    
    Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
 docs/HTTP2_ANDROID.md                              |  2 +-
 .../financial-benchmark-service/README.md          | 38 +++++++++++
 .../financial-benchmark-service/src/finbench_mcp.c | 78 +++++++++++++++++-----
 .../financial-benchmark-service/src/finbench_mcp.h | 51 ++++++++++++++
 4 files changed, 152 insertions(+), 17 deletions(-)

diff --git a/docs/HTTP2_ANDROID.md b/docs/HTTP2_ANDROID.md
index 59cfb8b70..7798b9cee 100644
--- a/docs/HTTP2_ANDROID.md
+++ b/docs/HTTP2_ANDROID.md
@@ -623,7 +623,7 @@ repository's registry**. What it does touch, using `composeCovariance` on
 |---|---|
 | `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`, as `covarianceFromReturns` is the only one that carries `returns`) |
 | `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` |
+| `finbench_mcp.c` | The tool's `inputSchema` constant, a row in the tool table, and a branch in `tools/call`. An operation that belongs to the application rather than to the sample goes through the weak `finbench_mcp_extra_tools()` / `finbench_mcp_extra_dispatch()` hook instead, so the sample stays unedited |
 | `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 | Nothing, if it dispatches through `finbench_dispatch_json_obj()` as the Kanaha Calcs adapter does; the router is upstream. An adapter with its own shape rules needs one more |
diff --git a/samples/user_guide/financial-benchmark-service/README.md b/samples/user_guide/financial-benchmark-service/README.md
index 3f7f881bb..c9826e4e1 100644
--- a/samples/user_guide/financial-benchmark-service/README.md
+++ b/samples/user_guide/financial-benchmark-service/README.md
@@ -256,6 +256,44 @@ Demonstrates O(1) hash table lookups vs O(n) linear search — a common optimiza
 
 Returns service capabilities, device info, and memory usage.
 
+## MCP Tools From an Embedding Application
+
+The MCP stdio transport (`finbench_mcp.c`) exposes this service's operations
+as tools. An application that links the transport into a binary of its own can
+add tools without editing it, by defining two functions the transport declares
+weak:
+
+```c
+#include "finbench_mcp.h"
+
+static const finbench_mcp_tool_t my_tools[] = {
+    { "covarianceFromCsv", "Read a CSV this device holds ...", "{\"type\":\"object\", ...}" },
+    { NULL, NULL, NULL }
+};
+
+const finbench_mcp_tool_t *finbench_mcp_extra_tools(void) { return my_tools; }
+
+axis2_char_t *finbench_mcp_extra_dispatch(const axutil_env_t *env,
+                                          const axis2_char_t *tool_name,
+                                          const axis2_char_t *args_json)
+{
+    if (strcmp(tool_name, "covarianceFromCsv") != 0) return NULL;  /* not ours */
+    return my_csv_covariance(env, args_json);   /* env-allocated; caller frees */
+}
+```
+
+`tools/list` then reports both catalogs and `tools/call` tries the built-ins
+first, so an extra tool cannot shadow one of these operations. The defaults
+return NULL, so a plain build of this sample is unchanged. This is the same
+weak-symbol arrangement the Android static service registry uses, and it needs
+a toolchain that has weak symbols; elsewhere the defaults win and the extra
+catalog is simply absent.
+
+Why it exists: this sample has no data of its own. An application that does —
+a phone holding a CSV of closes, say — can keep the reading and the path
+validation on its side of the line, where the file and its threat model live,
+and still reach `covarianceFromReturns` through the same transport.
+
 ## Building
 
 ### Prerequisites
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 6015203c9..6dbadc866 100644
--- a/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
+++ b/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
@@ -237,12 +237,6 @@ static const char SCHEMA_SCENARIO_ANALYSIS[] =
  * ============================================================================
  */
 
-typedef struct {
-    const char *name;
-    const char *description;
-    const char *input_schema_json;
-} finbench_mcp_tool_t;
-
 static const finbench_mcp_tool_t finbench_mcp_tools[] = {
     {
         "portfolioVariance",
@@ -300,6 +294,40 @@ static const finbench_mcp_tool_t finbench_mcp_tools[] = {
     { NULL, NULL, NULL }  /* sentinel */
 };
 
+/* ============================================================================
+ * Embedder extension point
+ *
+ * An application that links this transport can add tools of its own by
+ * defining these two functions; the defaults below are weak and expose
+ * nothing, so the sample on its own lists only its own operations. The
+ * mechanism is the one the Android static service registry already uses: a
+ * weak default that a strong definition in the application replaces at link
+ * time. On a toolchain without weak symbols the defaults simply win.
+ * ============================================================================
+ */
+
+#if defined(__GNUC__) || defined(__clang__)
+#define FINBENCH_MCP_WEAK   __attribute__((weak))
+#else
+#define FINBENCH_MCP_WEAK
+#endif
+
+FINBENCH_MCP_WEAK AXIS2_EXTERN const finbench_mcp_tool_t* AXIS2_CALL
+finbench_mcp_extra_tools(void)
+{
+    return NULL;
+}
+
+FINBENCH_MCP_WEAK AXIS2_EXTERN axis2_char_t* AXIS2_CALL
+finbench_mcp_extra_dispatch(
+    const axutil_env_t *env,
+    const axis2_char_t *tool_name,
+    const axis2_char_t *args_json)
+{
+    (void)env; (void)tool_name; (void)args_json;
+    return NULL;
+}
+
 /* Maximum request size — prevents DoS via unbounded memory allocation */
 #define MAX_MCP_REQUEST_BYTES   (16 * 1024 * 1024)  /* 16 MB per line */
 #define MCP_LINE_INITIAL_CAP    4096
@@ -391,22 +419,25 @@ static json_object *mcp_handle_initialize(void)
     return result;
 }
 
-static json_object *mcp_handle_tools_list(void)
+/* Append one NULL-terminated catalog to the tools array. */
+static void mcp_append_tools(
+    json_object                 *tools_array,
+    const finbench_mcp_tool_t   *catalog)
 {
-    json_object *tools_array = json_object_new_array();
+    if (!catalog) return;
 
     /* Iterate to NULL sentinel — no hardcoded count needed */
-    for (const finbench_mcp_tool_t *tool = finbench_mcp_tools;
-         tool->name != NULL; tool++) {
+    for (const finbench_mcp_tool_t *tool = catalog; tool->name != NULL; tool++) {
         json_object *tool_obj = json_object_new_object();
 
         json_object_object_add(tool_obj, "name",
             json_object_new_string(tool->name));
         json_object_object_add(tool_obj, "description",
-            json_object_new_string(tool->description));
+            json_object_new_string(tool->description ? tool->description : ""));
 
         /* Parse the inlined schema string into a json_object */
-        json_object *schema = json_tokener_parse(tool->input_schema_json);
+        json_object *schema = tool->input_schema_json
+            ? json_tokener_parse(tool->input_schema_json) : NULL;
         if (!schema) {
             /* Fallback: minimal valid schema (should never happen) */
             schema = json_object_new_object();
@@ -416,6 +447,14 @@ static json_object *mcp_handle_tools_list(void)
 
         json_object_array_add(tools_array, tool_obj);
     }
+}
+
+static json_object *mcp_handle_tools_list(void)
+{
+    json_object *tools_array = json_object_new_array();
+
+    mcp_append_tools(tools_array, finbench_mcp_tools);
+    mcp_append_tools(tools_array, finbench_mcp_extra_tools());
 
     json_object *result = json_object_new_object();
     json_object_object_add(result, "tools", tools_array);
@@ -487,10 +526,17 @@ static json_object *mcp_handle_tools_call(
     } else if (strcmp(tool_name, "scenarioAnalysis") == 0) {
         result_json = finbench_scenario_json_only(env, args_json);
     } else {
-        AXIS2_FREE(env->allocator, args_json);
-        *out_code = MCP_ERR_METHOD_NOT_FOUND;
-        *out_msg  = "Unknown tool name. Available: portfolioVariance, composeCovariance, covarianceFromReturns, monteCarlo, scenarioAnalysis";
-        return NULL;
+        /* Not one of ours: give the embedder's catalog a turn before failing. */
+        result_json = finbench_mcp_extra_dispatch(env, tool_name, args_json);
+        if (!result_json) {
+            AXIS2_FREE(env->allocator, args_json);
+            *out_code = MCP_ERR_METHOD_NOT_FOUND;
+            *out_msg  = "Unknown tool name. Built-in tools: portfolioVariance, "
+                        "composeCovariance, covarianceFromReturns, monteCarlo, "
+                        "scenarioAnalysis; an embedder may add more (tools/list "
+                        "is the catalog)";
+            return NULL;
+        }
     }
 
     AXIS2_FREE(env->allocator, args_json);
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 75b2b41a8..39227e4b7 100644
--- a/samples/user_guide/financial-benchmark-service/src/finbench_mcp.h
+++ b/samples/user_guide/financial-benchmark-service/src/finbench_mcp.h
@@ -75,6 +75,57 @@ extern "C" {
  */
 void finbench_run_mcp_stdio(const axutil_env_t *env);
 
+/**
+ * @brief One MCP tool: its name, its description, and the JSON Schema for its
+ *        arguments as a string.
+ *
+ * The built-in catalog is an array of these ending in a { NULL, NULL, NULL }
+ * sentinel. An embedder's extra catalog has the same shape.
+ */
+typedef struct finbench_mcp_tool {
+    const char *name;
+    const char *description;
+    const char *input_schema_json;
+} finbench_mcp_tool_t;
+
+/**
+ * @brief Extra tools contributed by the application embedding this transport.
+ *
+ * The default definition is weak and returns NULL, so a plain build of this
+ * sample exposes only its own operations. An application that links the
+ * transport can define this function (and finbench_mcp_extra_dispatch) to add
+ * tools of its own: the Kanaha Calcs app, for one, adds a tool that reads a
+ * CSV the phone already holds and hands the returns to covarianceFromReturns,
+ * which this sample cannot do because it has no file of its own to read.
+ *
+ * Same contract as the built-in catalog: a static array ending in a
+ * { NULL, NULL, NULL } sentinel, or NULL for none. Requires a toolchain with
+ * weak symbols (GCC, Clang); elsewhere the defaults cannot be overridden.
+ *
+ * @return Array of tools ending in a NULL-name sentinel, or NULL.
+ */
+AXIS2_EXTERN const finbench_mcp_tool_t* AXIS2_CALL
+finbench_mcp_extra_tools(void);
+
+/**
+ * @brief Run one of the extra tools.
+ *
+ * Called only for a tool name that is not one of the built-ins. The default
+ * definition is weak and returns NULL, which the caller reports as an unknown
+ * tool.
+ *
+ * @param env       Axis2/C environment.
+ * @param tool_name The tool being called.
+ * @param args_json The arguments object, serialized.
+ * @return Response JSON allocated with the env's allocator (the caller frees
+ *         it with AXIS2_FREE), or NULL if this tool is not known here.
+ */
+AXIS2_EXTERN axis2_char_t* AXIS2_CALL
+finbench_mcp_extra_dispatch(
+    const axutil_env_t *env,
+    const axis2_char_t *tool_name,
+    const axis2_char_t *args_json);
+
 #ifdef __cplusplus
 }
 #endif