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/10/20 17:34:49 UTC
(axis-axis2-c-core) branch master updated: Refuse finbench inputs the service used to default or drop
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
The following commit(s) were added to refs/heads/master by this push:
new 68b458146 Refuse finbench inputs the service used to default or drop
68b458146 is described below
commit 68b4581468e14804ef554a4154df31cfcc4e01fd
Author: Robert Lazarski <ro...@gmail.com>
AuthorDate: Mon Oct 19 16:30:46 2026 -1000
Refuse finbench inputs the service used to default or drop
model, percentiles, prob_tolerance, check_positive_definite, asset_ids,
covariance_basis and scenarioAnalysis n_assets are now refused with a
named error when wrong, instead of being replaced, truncated or dropped;
use_hash_lookup, which never changed anything, is refused. The MCP and
services.xml schemas now state the limits the code enforces.
Co-Authored-By: Claude Opus 5.5 <no...@anthropic.com>
---
docs/MCP_EXAMPLES.md | 5 +-
.../financial-benchmark-service/services.xml | 69 ++---
.../src/financial_benchmark_service.c | 280 +++++++++++++++------
.../src/financial_benchmark_service.h | 52 +++-
.../financial-benchmark-service/src/finbench_mcp.c | 73 +++---
5 files changed, 318 insertions(+), 161 deletions(-)
diff --git a/docs/MCP_EXAMPLES.md b/docs/MCP_EXAMPLES.md
index 78d794ccd..c0395611a 100644
--- a/docs/MCP_EXAMPLES.md
+++ b/docs/MCP_EXAMPLES.md
@@ -512,8 +512,7 @@ curl -k --http2 -s \
{"price": 180.0, "probability": 0.10}
]
}
- ],
- "use_hash_lookup": true
+ ]
}' \
https://10.10.10.10/services/FinancialBenchmarkService/scenarioAnalysis
```
@@ -535,7 +534,7 @@ curl -k --http2 -s \
**MCP stdio:**
```bash
-echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"scenarioAnalysis","arguments":{"n_assets":3,"assets":[{"asset_id":1,"current_price":150.0,"position_size":100,"scenarios":[{"price":165.0,"probability":0.15},{"price":157.5,"probability":0.25},{"price":150.0,"probability":0.30},{"price":142.5,"probability":0.20},{"price":135.0,"probability":0.10}]},{"asset_id":2,"current_price":75.0,"position_size":200,"scenarios":[{"price":82.5,"probability":0.15},{"price":78.75,"prob [...]
+echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"scenarioAnalysis","arguments":{"n_assets":3,"assets":[{"asset_id":1,"current_price":150.0,"position_size":100,"scenarios":[{"price":165.0,"probability":0.15},{"price":157.5,"probability":0.25},{"price":150.0,"probability":0.30},{"price":142.5,"probability":0.20},{"price":135.0,"probability":0.10}]},{"asset_id":2,"current_price":75.0,"position_size":200,"scenarios":[{"price":82.5,"probability":0.15},{"price":78.75,"prob [...]
| /usr/local/axis2c/bin/financial-benchmark-mcp
```
diff --git a/samples/user_guide/financial-benchmark-service/services.xml b/samples/user_guide/financial-benchmark-service/services.xml
index a4536fcb1..abf895674 100644
--- a/samples/user_guide/financial-benchmark-service/services.xml
+++ b/samples/user_guide/financial-benchmark-service/services.xml
@@ -86,18 +86,20 @@
<parameter name="mcpIdempotent">true</parameter>
<parameter name="mcpInputSchema">{
"type": "object",
- "required": ["n_assets", "weights", "covariance_matrix"],
+ "required": ["weights", "covariance_matrix"],
"properties": {
- "n_assets": {"type": "integer", "minimum": 2, "maximum": 2000,
- "description": "Number of assets in the portfolio"},
+ "n_assets": {"type": "integer", "minimum": 1, "maximum": 3000,
+ "description": "Number of assets in the portfolio. Inferred from weights if omitted; if given it must equal the weights length."},
"weights": {"type": "array", "items": {"type": "number"},
"description": "Exactly n_assets values. If normalize_weights=true they are scaled to sum=1 before calculation; otherwise caller must ensure they already sum to 1."},
"covariance_matrix": {"type": "array", "items": {"type": "number"},
"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,
+ "n_periods_per_year": {"type": "integer", "minimum": 1, "maximum": 10000000, "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."},
+ "covariance_basis": {"type": "string", "enum": ["annualized", "per_period"],
+ "description": "Optional label for the matrix's basis, as the covariance producers echo it. \"annualized\" with n_periods_per_year other than 1 is refused; the value applied is always n_periods_per_year."},
"request_id": {"type": "string", "description": "Optional correlation ID echoed in the response"}
}
}</parameter>
@@ -169,7 +171,7 @@
"check_positive_definite": {"type": "boolean", "default": true,
"description": "Cholesky check (O(n³)). Refuses a matrix that is not positive definite."},
"asset_ids": {"type": "array", "items": {"type": "string"},
- "description": "Optional labels, one per asset, echoed back."},
+ "description": "Optional labels, exactly one string per asset, echoed back."},
"request_id": {"type": "string", "description": "Optional correlation ID echoed in the response"}
}
}</parameter>
@@ -231,12 +233,12 @@
"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,
+ "n_periods_per_year": {"type": "number", "exclusiveMinimum": 0, "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."},
+ "description": "Optional labels, exactly one string per asset, echoed back."},
"request_id": {"type": "string", "description": "Optional correlation ID echoed in the response"}
}
}</parameter>
@@ -298,20 +300,21 @@
<parameter name="mcpIdempotent">false</parameter>
<parameter name="mcpInputSchema">{
"type": "object",
- "required": ["n_simulations", "n_periods", "initial_value", "expected_return", "volatility"],
+ "required": [],
+ "description": "All fields have defaults; an empty {} request body is valid.",
"properties": {
- "n_simulations": {"type": "integer", "minimum": 100, "maximum": 1000000,
+ "n_simulations": {"type": "integer", "minimum": 1, "maximum": 1000000, "default": 10000,
"description": "Number of simulation paths"},
- "n_periods": {"type": "integer", "minimum": 1, "maximum": 2520,
- "description": "Number of time steps (252 = 1 trading year)"},
- "initial_value": {"type": "number", "description": "Initial portfolio value"},
- "expected_return": {"type": "number", "description": "Annualised expected return (e.g. 0.08 for 8%)"},
- "volatility": {"type": "number", "description": "Annualised volatility (e.g. 0.20 for 20%)"},
+ "n_periods": {"type": "integer", "minimum": 1, "maximum": 100000, "default": 252,
+ "description": "Number of time steps (252 = 1 trading year). n_simulations × n_periods (× n_assets for a book) may not exceed 1e9."},
+ "initial_value": {"type": "number", "exclusiveMinimum": 0, "default": 1000000, "description": "Initial portfolio value"},
+ "expected_return": {"type": "number", "default": 0.08, "description": "Annualised expected return (e.g. 0.08 for 8%)"},
+ "volatility": {"type": "number", "minimum": 0, "default": 0.20, "description": "Annualised volatility (e.g. 0.20 for 20%)"},
"random_seed": {"type": "integer", "description": "Seed for reproducible results. When omitted, seeded from time() — results differ across calls."},
- "n_periods_per_year": {"type": "integer", "default": 252,
+ "n_periods_per_year": {"type": "integer", "minimum": 1, "maximum": 10000000, "default": 252,
"description": "Trading periods per year; dt = 1/n_periods_per_year. Must match the basis of expected_return and volatility."},
- "percentiles": {"type": "array", "items": {"type": "number"}, "maxItems": 8,
- "description": "Tail levels for VaR, e.g. [0.01, 0.05] for 99% and 95%. Default [0.01, 0.05]."},
+ "percentiles": {"type": "array", "items": {"type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 1}, "maxItems": 8,
+ "description": "Tail levels for VaR, e.g. [0.01, 0.05] for 99% and 95%. Max 8 values, each strictly between 0 and 1. Default [0.01, 0.05]."},
"model": {"type": "string", "enum": ["gbm", "merton"], "default": "gbm"},
"jump_intensity": {"type": "number", "default": 1.0, "description": "Merton: jumps per year (lambda*dt must be at most 0.1)."},
"jump_mean": {"type": "number", "default": -0.03, "description": "Merton: mean log-jump."},
@@ -320,7 +323,7 @@
"description": "Correlated book: n_assets×n_assets annualized covariance matrix (flat row-major or nested), as composeCovariance returns it. Switches the run to a buy-and-hold multi-asset book; volatility is then ignored. Must be positive definite."},
"weights": {"type": "array", "items": {"type": "number"},
"description": "Correlated book: n_assets weights ≥ 0 (long-only), summing to 1 unless normalize_weights."},
- "n_assets": {"type": "integer", "maximum": 100, "description": "Correlated book: inferred from weights if omitted."},
+ "n_assets": {"type": "integer", "minimum": 1, "maximum": 100, "description": "Correlated book: inferred from weights if omitted."},
"expected_returns": {"type": "array", "items": {"type": "number"},
"description": "Correlated book: per-asset drifts. Default: expected_return for every asset."},
"normalize_weights": {"type": "boolean", "default": false},
@@ -391,20 +394,20 @@
<parameter name="mcpIdempotent">true</parameter>
<parameter name="mcpInputSchema">{
"type": "object",
- "required": ["n_assets", "assets"],
+ "required": ["assets"],
"properties": {
- "n_assets": {"type": "integer", "minimum": 1, "maximum": 2000,
- "description": "Number of assets"},
+ "n_assets": {"type": "integer", "minimum": 1, "maximum": 3000,
+ "description": "Optional; if given it must equal the length of assets."},
"assets": {"type": "array",
- "minItems": 1, "maxItems": 2000,
- "description": "Array of asset scenario objects. Length must equal n_assets.",
+ "minItems": 1, "maxItems": 3000,
+ "description": "Array of asset scenario objects.",
"items": {
"type": "object",
- "required": ["asset_id", "current_price", "scenarios", "position_size"],
+ "required": ["current_price", "scenarios", "position_size"],
"properties": {
- "asset_id": {"type": "integer"},
- "current_price": {"type": "number"},
- "scenarios": {"type": "array", "minItems": 2,
+ "asset_id": {"type": "integer", "description": "Unique asset identifier. Default: 1-based position in assets."},
+ "current_price": {"type": "number", "exclusiveMinimum": 0},
+ "scenarios": {"type": "array", "minItems": 1, "maxItems": 10,
"description": "One entry per scenario; probabilities must sum to 1.0.",
"items": {"type": "object",
"required": ["price", "probability"],
@@ -412,11 +415,11 @@
"price": {"type": "number", "description": "Target price in this scenario"},
"probability": {"type": "number", "description": "Probability weight of this scenario"}
}}},
- "position_size": {"type": "number", "description": "Shares held (integer positions; fractional positions not supported by C implementation)"}
+ "position_size": {"type": "number", "exclusiveMinimum": 0, "description": "Units held, > 0 (long only)"}
}
}},
- "use_hash_lookup": {"type": "boolean", "default": true,
- "description": "Use O(1) hash lookup instead of O(n) linear scan"},
+ "prob_tolerance": {"type": "number", "exclusiveMinimum": 0, "maximum": 0.1, "default": 0.0001,
+ "description": "Tolerance for probability sum validation per asset. Must be greater than 0 and at most 0.1."},
"request_id": {"type": "string"}
}
}</parameter>
@@ -445,7 +448,6 @@
},
...
],
- "use_hash_lookup": true,
"request_id": "scenario-001"
}
</description>
@@ -486,7 +488,7 @@
"service_name": "FinancialBenchmarkService",
"version": "1.0.0",
"operations": ["portfolioVariance", "composeCovariance", "covarianceFromReturns", "monteCarlo", ...],
- "max_assets": 2000,
+ "max_assets": 3000,
"max_simulations": 1000000,
"memory_constraint_mb": 512,
"current_memory_kb": 28456,
@@ -514,9 +516,8 @@
<parameter name="mcpIdempotent">true</parameter>
<parameter name="mcpInputSchema">{
"type": "object",
- "required": ["n_assets"],
"properties": {
- "n_assets": {"type": "integer", "minimum": 2, "maximum": 2000,
+ "n_assets": {"type": "integer", "minimum": 1, "maximum": 3000, "default": 100,
"description": "Number of assets to generate"}
}
}</parameter>
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 d6740c880..f67a3eead 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
@@ -200,6 +200,79 @@ finbench_read_periods_per_year(json_object *json_obj, double def,
#define FINBENCH_PERIODS_INVALID_MSG \
"n_periods_per_year must be a number > 0%s; omit it for the default."
+/* Record a refused field in a request's input_error buffer. The first
+ * refusal is kept, so the caller sees the earliest problem in the request.
+ * The calculation returns it as the error_message before doing any work. */
+static void
+finbench_input_error(char *buf, const char *fmt, ...)
+{
+ va_list ap;
+
+ if (buf[0])
+ return;
+ va_start(ap, fmt);
+ vsnprintf(buf, FINBENCH_INPUT_ERROR_LEN, fmt, ap);
+ va_end(ap);
+}
+
+/* check_positive_definite: absent means true; otherwise it must be a JSON
+ * boolean. json_object_get_boolean would read the string "false" as true. */
+static axis2_bool_t
+finbench_read_check_pd(json_object *json_obj, char *err)
+{
+ json_object *v = NULL;
+
+ if (!json_object_object_get_ex(json_obj, "check_positive_definite", &v))
+ return AXIS2_TRUE;
+ if (!json_object_is_type(v, json_type_boolean)) {
+ finbench_input_error(err, "check_positive_definite must be true or false.");
+ return AXIS2_TRUE;
+ }
+ return json_object_get_boolean(v) ? AXIS2_TRUE : AXIS2_FALSE;
+}
+
+/* asset_ids: absent, or an array of exactly n strings. A wrong length or a
+ * non-string entry is refused rather than dropped or echoed as text. */
+static char **
+finbench_read_asset_ids(const axutil_env_t *env, json_object *json_obj,
+ int n, char *err)
+{
+ json_object *arr = NULL;
+ char **ids;
+ int i, len;
+
+ if (!json_object_object_get_ex(json_obj, "asset_ids", &arr))
+ return NULL;
+ if (!json_object_is_type(arr, json_type_array)) {
+ finbench_input_error(err,
+ "asset_ids must be an array of %d strings, one per asset.", n);
+ return NULL;
+ }
+ len = (int)json_object_array_length(arr);
+ if (len != n) {
+ finbench_input_error(err,
+ "asset_ids has %d entries but n_assets is %d; give one string per asset.",
+ len, n);
+ return NULL;
+ }
+ for (i = 0; i < n; i++) {
+ if (!json_object_is_type(json_object_array_get_idx(arr, i), json_type_string)) {
+ finbench_input_error(err, "asset_ids[%d] must be a string.", i);
+ return NULL;
+ }
+ }
+ ids = AXIS2_MALLOC(env->allocator, (size_t)n * sizeof(char *));
+ if (!ids) {
+ finbench_input_error(err, "Memory allocation failed for asset_ids.");
+ return NULL;
+ }
+ for (i = 0; i < n; i++) {
+ ids[i] = axutil_strdup(env,
+ json_object_get_string(json_object_array_get_idx(arr, i)));
+ }
+ return ids;
+}
+
/* ============================================================================
* Portfolio Variance Implementation
*
@@ -373,11 +446,17 @@ finbench_portfolio_variance_request_create_from_json(
* 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;
+ if (json_object_object_get_ex(json_obj, "covariance_basis", &value_obj)) {
+ const char *basis = json_object_is_type(value_obj, json_type_string)
+ ? json_object_get_string(value_obj) : NULL;
+ if (!basis || (strcmp(basis, "annualized") != 0 &&
+ strcmp(basis, "per_period") != 0)) {
+ finbench_input_error(request->input_error,
+ "covariance_basis must be \"annualized\" or \"per_period\".");
+ } else if (request->n_periods_per_year != 1 &&
+ strcmp(basis, "annualized") == 0) {
+ request->basis_conflict = AXIS2_TRUE;
+ }
}
json_object_put(json_obj);
@@ -500,6 +579,14 @@ finbench_calculate_portfolio_variance(
n = request->n_assets;
+ if (request->input_error[0]) {
+ response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+ response->error_message = axutil_strdup(env, request->input_error);
+ AXIS2_LOG_ERROR(env->log, AXIS2_LOG_SI, "FinBench portfolioVariance: %s",
+ request->input_error);
+ return response;
+ }
+
if (request->n_periods_per_year_invalid) {
char err_buf[160];
snprintf(err_buf, sizeof(err_buf), FINBENCH_PERIODS_INVALID_MSG,
@@ -1047,26 +1134,12 @@ finbench_compose_covariance_request_create_from_json(
}
/* check_positive_definite — default true */
- if (json_object_object_get_ex(json_obj, "check_positive_definite", &value_obj)) {
- request->check_positive_definite = json_object_get_boolean(value_obj)
- ? AXIS2_TRUE : AXIS2_FALSE;
- }
+ request->check_positive_definite =
+ finbench_read_check_pd(json_obj, request->input_error);
- /* 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;
- }
- }
- }
+ /* asset_ids (optional; exactly n_assets strings when present) */
+ request->asset_ids = finbench_read_asset_ids(env, json_obj,
+ request->n_assets, request->input_error);
if (json_object_object_get_ex(json_obj, "request_id", &value_obj)) {
const char *rid = json_object_get_string(value_obj);
@@ -1204,6 +1277,9 @@ finbench_compose_covariance(
if (!request || !request->volatilities) {
return compose_fail(env, response, "Invalid request parameters");
}
+ if (request->input_error[0]) {
+ return compose_fail(env, response, "%s", request->input_error);
+ }
n = request->n_assets;
response->n_assets = n;
@@ -1698,26 +1774,12 @@ finbench_covariance_from_returns_request_create_from_json(
0, &request->n_periods_per_year))
request->n_periods_per_year_invalid = AXIS2_TRUE;
- if (json_object_object_get_ex(json_obj, "check_positive_definite", &value_obj)) {
- request->check_positive_definite = json_object_get_boolean(value_obj)
- ? AXIS2_TRUE : AXIS2_FALSE;
- }
+ request->check_positive_definite =
+ finbench_read_check_pd(json_obj, request->input_error);
- /* 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;
- }
- }
- }
+ /* asset_ids (optional; exactly n_assets strings when present) */
+ request->asset_ids = finbench_read_asset_ids(env, json_obj,
+ request->n_assets, request->input_error);
if (json_object_object_get_ex(json_obj, "request_id", &value_obj)) {
const char *rid = json_object_get_string(value_obj);
@@ -1822,6 +1884,9 @@ finbench_covariance_from_returns(
if (!request || !request->returns) {
return cfr_fail(env, response, "Invalid request parameters");
}
+ if (request->input_error[0]) {
+ return cfr_fail(env, response, "%s", request->input_error);
+ }
n = request->n_assets;
obs = request->n_obs;
@@ -2256,19 +2321,40 @@ finbench_monte_carlo_request_create_from_json(
request->n_periods_per_year = (int)npy;
}
- /* percentiles — array of tail levels in (0,1); default {0.01, 0.05} */
+ /* percentiles — array of up to FINBENCH_MAX_PERCENTILES numbers, each
+ * strictly inside (0,1); default {0.01, 0.05}. An extra or out-of-range
+ * entry is refused: dropping it returned SUCCESS without the VaR asked for. */
{
json_object *pct_arr = NULL;
- if (json_object_object_get_ex(json_obj, "percentiles", &pct_arr) &&
- json_object_is_type(pct_arr, json_type_array)) {
- int n_pct = json_object_array_length(pct_arr);
- if (n_pct > FINBENCH_MAX_PERCENTILES) n_pct = FINBENCH_MAX_PERCENTILES;
+ if (json_object_object_get_ex(json_obj, "percentiles", &pct_arr)) {
+ int n_pct, pi;
request->n_percentiles = 0;
- int pi;
- for (pi = 0; pi < n_pct; pi++) {
- json_object *pct_elem = json_object_array_get_idx(pct_arr, pi);
- double p = json_object_get_double(pct_elem);
- if (p > 0.0 && p < 1.0) {
+ if (!json_object_is_type(pct_arr, json_type_array)) {
+ finbench_input_error(request->input_error,
+ "percentiles must be an array of up to %d numbers in (0, 1).",
+ FINBENCH_MAX_PERCENTILES);
+ } else if ((n_pct = (int)json_object_array_length(pct_arr)) >
+ FINBENCH_MAX_PERCENTILES) {
+ finbench_input_error(request->input_error,
+ "percentiles has %d entries; the maximum is %d.",
+ n_pct, FINBENCH_MAX_PERCENTILES);
+ } else {
+ for (pi = 0; pi < n_pct; pi++) {
+ json_object *pct_elem = json_object_array_get_idx(pct_arr, pi);
+ double p;
+ if (!json_object_is_type(pct_elem, json_type_double) &&
+ !json_object_is_type(pct_elem, json_type_int)) {
+ finbench_input_error(request->input_error,
+ "percentiles[%d] must be a number in (0, 1).", pi);
+ break;
+ }
+ p = json_object_get_double(pct_elem);
+ if (!(p > 0.0 && p < 1.0)) {
+ finbench_input_error(request->input_error,
+ "percentiles[%d] = %g must be strictly between 0 and 1 "
+ "(0.05 for a 95%% VaR).", pi, p);
+ break;
+ }
request->percentiles[request->n_percentiles++] = p;
}
}
@@ -2280,17 +2366,18 @@ finbench_monte_carlo_request_create_from_json(
}
}
- /* model — 0=GBM (default), 1=Merton jump-diffusion */
+ /* model — "gbm" (default) or "merton", exactly the schema's enum. Any
+ * other value is refused; it used to run GBM without saying so. */
+ request->model = FINBENCH_MODEL_GBM;
if (json_object_object_get_ex(json_obj, "model", &value_obj)) {
- const char *model_str = json_object_get_string(value_obj);
- if (model_str && (strcmp(model_str, "merton") == 0 ||
- strcmp(model_str, "MERTON") == 0)) {
+ const char *model_str = json_object_is_type(value_obj, json_type_string)
+ ? json_object_get_string(value_obj) : NULL;
+ if (model_str && strcmp(model_str, "merton") == 0) {
request->model = FINBENCH_MODEL_MERTON;
- } else {
- request->model = FINBENCH_MODEL_GBM;
+ } else if (!model_str || strcmp(model_str, "gbm") != 0) {
+ finbench_input_error(request->input_error,
+ "model must be \"gbm\" or \"merton\".");
}
- } else {
- request->model = FINBENCH_MODEL_GBM;
}
/* Jump-diffusion parameters (Merton only; harmless defaults for GBM) */
@@ -2413,11 +2500,8 @@ finbench_monte_carlo_request_create_from_json(
}
}
- /* Validate limits */
- if (request->n_simulations > FINBENCH_MAX_SIMULATIONS) {
- request->n_simulations = FINBENCH_MAX_SIMULATIONS;
- }
-
+ /* n_simulations above FINBENCH_MAX_SIMULATIONS is refused by
+ * finbench_run_monte_carlo, not clamped here. */
json_object_put(json_obj);
return request;
}
@@ -2954,6 +3038,12 @@ finbench_run_monte_carlo(
return response;
}
+ if (request->input_error[0]) {
+ response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+ response->error_message = axutil_strdup(env, request->input_error);
+ return response;
+ }
+
if (request->n_periods_per_year_invalid) {
char err_buf[160];
snprintf(err_buf, sizeof(err_buf), FINBENCH_PERIODS_INVALID_MSG,
@@ -3328,7 +3418,7 @@ finbench_monte_carlo_json_only(
"volatility (float, default 0.20), "
"random_seed (int, default 0=random), "
"n_periods_per_year (int, default 252), "
- "percentiles (float[], default [0.01, 0.05]), "
+ "percentiles (up to 8 floats in (0, 1), default [0.01, 0.05]), "
"model (string, 'gbm' or 'merton', default 'gbm'), "
"correlated book: covariance_matrix (float[n*n] or float[n][n]) + weights (float[n] >= 0), "
"optional expected_returns (float[n]), normalize_weights (bool); "
@@ -3389,25 +3479,33 @@ finbench_scenario_request_create_from_json(
}
memset(request, 0, sizeof(finbench_scenario_request_t));
- /* use_hash_lookup flag (default: run both, report both) */
- if (json_object_object_get_ex(json_obj, "use_hash_lookup", &value_obj)) {
- request->use_hash_lookup = json_object_get_boolean(value_obj)
- ? AXIS2_TRUE : AXIS2_FALSE;
- } else {
- request->use_hash_lookup = AXIS2_TRUE;
+ /* use_hash_lookup is refused: the operation always times both the
+ * linear scan and the hash lookup and reports both, so the flag could
+ * only be ignored. */
+ if (json_object_object_get_ex(json_obj, "use_hash_lookup", NULL)) {
+ finbench_input_error(request->input_error,
+ "use_hash_lookup is not supported: scenarioAnalysis always times "
+ "both the linear scan and the hash lookup. Remove the field.");
}
/*
- * prob_tolerance — probability sum tolerance per asset.
- * Default 1e-4; 0.0 → default; clamped to [1e-10, 0.1].
+ * prob_tolerance — probability sum tolerance per asset. Default 1e-4;
+ * otherwise a number in (0, 0.1], refused outside it rather than
+ * replaced, so the tolerance applied is always the one asked for.
*/
+ request->prob_tolerance = 1e-4;
if (json_object_object_get_ex(json_obj, "prob_tolerance", &value_obj)) {
- double tol = json_object_get_double(value_obj);
- if (tol <= 0.0) tol = 1e-4;
- if (tol > 0.1) tol = 0.1;
- request->prob_tolerance = tol;
- } else {
- request->prob_tolerance = 1e-4;
+ double tol;
+ if (!json_object_is_type(value_obj, json_type_double) &&
+ !json_object_is_type(value_obj, json_type_int)) {
+ finbench_input_error(request->input_error,
+ "prob_tolerance must be a number in (0, 0.1].");
+ } else if (!((tol = json_object_get_double(value_obj)) > 0.0 && tol <= 0.1)) {
+ finbench_input_error(request->input_error,
+ "prob_tolerance = %g must be > 0 and <= 0.1 (default 0.0001).", tol);
+ } else {
+ request->prob_tolerance = tol;
+ }
}
/* request_id */
@@ -3439,6 +3537,16 @@ finbench_scenario_request_create_from_json(
}
request->n_assets = n_assets;
+
+ /* n_assets is optional; when given it must match the assets array. */
+ if (json_object_object_get_ex(json_obj, "n_assets", &value_obj) &&
+ (!json_object_is_type(value_obj, json_type_int) ||
+ json_object_get_int64(value_obj) != n_assets)) {
+ finbench_input_error(request->input_error,
+ "n_assets must equal the number of entries in assets (%d); "
+ "omit it to use that count.", n_assets);
+ }
+
request->assets = AXIS2_MALLOC(env->allocator,
n_assets * sizeof(finbench_asset_scenario_t));
if (!request->assets) {
@@ -3591,6 +3699,12 @@ finbench_calculate_scenarios(
return response;
}
+ if (request->input_error[0]) {
+ response->status = axutil_strdup(env, FINBENCH_STATUS_FAILED);
+ response->error_message = axutil_strdup(env, request->input_error);
+ return response;
+ }
+
/* -----------------------------------------------------------------------
* Step 1: Validate probability distributions before any computation.
*
@@ -3981,8 +4095,8 @@ finbench_scenario_json_only(
"\"Failed to parse scenarioAnalysis request. "
"Required: assets (array of {asset_id, current_price, position_size, "
"scenarios: [{price, probability}]}). "
- "Optional: use_hash_lookup (bool, default true), "
- "prob_tolerance (float, default 0.0001), "
+ "Optional: n_assets (int, must equal the assets count), "
+ "prob_tolerance (float in (0, 0.1], default 0.0001), "
"request_id (string).\"}");
}
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 e61d8a2d8..32cd574b9 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
@@ -119,6 +119,9 @@ extern "C"
#define FINBENCH_STATUS_SUCCESS "SUCCESS"
#define FINBENCH_STATUS_FAILED "FAILED"
+/** Size of the input_error buffer each request struct carries. */
+#define FINBENCH_INPUT_ERROR_LEN 256
+
/* ============================================================================
* Portfolio Variance Calculation
*
@@ -236,6 +239,13 @@ typedef struct finbench_portfolio_variance_request
/** n_periods_per_year was present but not a positive whole number; refused. */
axis2_bool_t n_periods_per_year_invalid;
+ /**
+ * First refused optional field, as the error_message to return; empty
+ * when the request parsed cleanly. A field present with the wrong type
+ * or an out-of-range value is refused rather than defaulted.
+ */
+ char input_error[FINBENCH_INPUT_ERROR_LEN];
+
} finbench_portfolio_variance_request_t;
/**
@@ -374,6 +384,13 @@ typedef struct finbench_compose_covariance_request
/** Request identifier for tracing */
char *request_id;
+ /**
+ * First refused optional field, as the error_message to return; empty
+ * when the request parsed cleanly. A field present with the wrong type
+ * or an out-of-range value is refused rather than defaulted.
+ */
+ char input_error[FINBENCH_INPUT_ERROR_LEN];
+
} finbench_compose_covariance_request_t;
/**
@@ -493,6 +510,13 @@ typedef struct finbench_covariance_from_returns_request
/** Request identifier for tracing */
char *request_id;
+ /**
+ * First refused optional field, as the error_message to return; empty
+ * when the request parsed cleanly. A field present with the wrong type
+ * or an out-of-range value is refused rather than defaulted.
+ */
+ char input_error[FINBENCH_INPUT_ERROR_LEN];
+
} finbench_covariance_from_returns_request_t;
/**
@@ -741,7 +765,8 @@ typedef struct finbench_monte_carlo_request
* Percentile levels for VaR reporting, values strictly in (0, 1). The
* service interprets p as a LOSS PROBABILITY, so 0.05 means "5% chance
* of losing at least VaR_p" (i.e., a 95% VaR). Defaults: {0.01, 0.05}.
- * Up to FINBENCH_MAX_PERCENTILES values; any beyond that are ignored.
+ * Up to FINBENCH_MAX_PERCENTILES values; the parser refuses more, and
+ * refuses a value that is not a number strictly inside (0, 1).
*
* Estimator: floor(p * n_sims). After ascending sort,
* VaR_p = initial_value - sorted_final_values[floor(p * n_sims)]
@@ -856,6 +881,13 @@ typedef struct finbench_monte_carlo_request
/** Request identifier */
char *request_id;
+ /**
+ * First refused optional field, as the error_message to return; empty
+ * when the request parsed cleanly. A field present with the wrong type
+ * or an out-of-range value is refused rather than defaulted.
+ */
+ char input_error[FINBENCH_INPUT_ERROR_LEN];
+
} finbench_monte_carlo_request_t;
/**
@@ -1026,23 +1058,27 @@ typedef struct finbench_scenario_request
/** Array of asset scenarios */
finbench_asset_scenario_t *assets;
- /** Enable hash table optimization (for comparison) */
- axis2_bool_t use_hash_lookup;
-
/**
* Tolerance for probability sum validation.
* Each asset's scenario probabilities must sum to 1.0 within this
* tolerance; the service rejects the whole request otherwise.
- * Default: 1e-4 (0.01%). Passing 0.0 or any negative value falls back
- * to this default. Values > 0.1 are capped at 0.1 to prevent an
- * overly-loose tolerance from hiding real probability-sum bugs
- * (e.g., three scenarios at 0.3 each summing to 0.9).
+ * Default: 1e-4 (0.01%). The parser refuses a value outside (0, 0.1]:
+ * a looser tolerance would hide real probability-sum bugs (e.g., three
+ * scenarios at 0.3 each summing to 0.9). A direct C caller that leaves
+ * it 0 gets the default.
*/
double prob_tolerance;
/** Request identifier */
char *request_id;
+ /**
+ * First refused optional field, as the error_message to return; empty
+ * when the request parsed cleanly. A field present with the wrong type
+ * or an out-of-range value is refused rather than defaulted.
+ */
+ char input_error[FINBENCH_INPUT_ERROR_LEN];
+
} finbench_scenario_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 c98e8550e..65ad758da 100644
--- a/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
+++ b/samples/user_guide/financial-benchmark-service/src/finbench_mcp.c
@@ -67,8 +67,9 @@ static const char SCHEMA_PORTFOLIO_VARIANCE[] =
"{"
"\"type\":\"object\","
"\"properties\":{"
- "\"n_assets\":{\"type\":\"integer\","
- "\"description\":\"Number of assets in the portfolio (max 3000)\"},"
+ "\"n_assets\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":3000,"
+ "\"description\":\"Number of assets in the portfolio (max 3000). Inferred from weights if "
+ "omitted; if given it must equal the weights length\"},"
"\"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\"},"
@@ -78,26 +79,30 @@ static const char SCHEMA_PORTFOLIO_VARIANCE[] =
"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\",\"default\":1,"
+ "\"n_periods_per_year\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":10000000,\"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. "
"Refused if the request also says covariance_basis \\\"annualized\\\"\"},"
+ "\"covariance_basis\":{\"type\":\"string\",\"enum\":[\"annualized\",\"per_period\"],"
+ "\"description\":\"Optional label for the matrix's basis, as the covariance producers echo it. "
+ "\\\"annualized\\\" with n_periods_per_year other than 1 is refused; the value applied "
+ "is always n_periods_per_year\"},"
"\"request_id\":{\"type\":\"string\","
"\"description\":\"Optional identifier echoed in the response for request tracing\"}"
"},"
- "\"required\":[\"n_assets\",\"weights\",\"covariance_matrix\"]"
+ "\"required\":[\"weights\",\"covariance_matrix\"]"
"}";
static const char SCHEMA_COMPOSE_COVARIANCE[] =
"{"
"\"type\":\"object\","
"\"properties\":{"
- "\"n_assets\":{\"type\":\"integer\","
+ "\"n_assets\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":500,"
"\"description\":\"Number of assets (max 500: the check is O(n^3) and the reply is O(n^2)). "
"Inferred from volatilities if omitted\"},"
"\"volatilities\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},"
- "\"description\":\"Per-asset volatilities, each > 0, as decimals (0.22 = 22%%). "
+ "\"description\":\"Per-asset volatilities, each > 0, as decimals (0.22 = 22%). "
"The covariance matrix inherits their time basis: annualized vols give an "
"annualized matrix, to be passed to portfolioVariance with n_periods_per_year=1\"},"
"\"correlation\":{\"type\":\"number\",\"minimum\":-1,\"maximum\":1,"
@@ -111,7 +116,7 @@ static const char SCHEMA_COMPOSE_COVARIANCE[] =
"\"description\":\"Run a Cholesky decomposition and refuse a matrix that is not "
"positive definite. Default true. O(n^3); set false only for very large n\"},"
"\"asset_ids\":{\"type\":\"array\",\"items\":{\"type\":\"string\"},"
- "\"description\":\"Optional labels, one per asset, echoed in the response\"},"
+ "\"description\":\"Optional labels, exactly one string per asset, echoed in the response\"},"
"\"request_id\":{\"type\":\"string\","
"\"description\":\"Optional identifier echoed in the response for request tracing\"}"
"},"
@@ -127,11 +132,11 @@ static const char SCHEMA_COVARIANCE_FROM_RETURNS[] =
"(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\","
+ "\"n_assets\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":50,"
"\"description\":\"Number of assets (max 50). Inferred from a nested returns array\"},"
- "\"n_obs\":{\"type\":\"integer\","
+ "\"n_obs\":{\"type\":\"integer\",\"minimum\":2,\"maximum\":5000,"
"\"description\":\"Observations per asset (max 5000). Inferred from the first row\"},"
- "\"n_periods_per_year\":{\"type\":\"number\","
+ "\"n_periods_per_year\":{\"type\":\"number\",\"exclusiveMinimum\":0,\"default\":252,"
"\"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 "
@@ -140,7 +145,7 @@ static const char SCHEMA_COVARIANCE_FROM_RETURNS[] =
"\"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\"},"
+ "\"description\":\"Optional labels, exactly one string per asset, echoed in the response\"},"
"\"request_id\":{\"type\":\"string\","
"\"description\":\"Optional identifier echoed in the response for request tracing\"}"
"},"
@@ -151,25 +156,27 @@ static const char SCHEMA_MONTE_CARLO[] =
"{"
"\"type\":\"object\","
"\"properties\":{"
- "\"n_simulations\":{\"type\":\"integer\","
+ "\"n_simulations\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":1000000,"
"\"description\":\"Number of simulation paths (max 1,000,000). Default: 10,000\"},"
- "\"n_periods\":{\"type\":\"integer\","
- "\"description\":\"Number of time steps per path (e.g. 252 = one trading year). Default: 252\"},"
- "\"initial_value\":{\"type\":\"number\","
+ "\"n_periods\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":100000,"
+ "\"description\":\"Number of time steps per path (e.g. 252 = one trading year). Default: 252. "
+ "n_simulations x n_periods (x n_assets for a book) may not exceed 1e9\"},"
+ "\"initial_value\":{\"type\":\"number\",\"exclusiveMinimum\":0,"
"\"description\":\"Initial portfolio value in currency units. Default: 1,000,000\"},"
"\"expected_return\":{\"type\":\"number\","
"\"description\":\"Expected annualized return (e.g. 0.08 for 8%). Default: 0.08\"},"
- "\"volatility\":{\"type\":\"number\","
+ "\"volatility\":{\"type\":\"number\",\"minimum\":0,"
"\"description\":\"Annualized volatility (e.g. 0.20 for 20%). Default: 0.20\"},"
"\"random_seed\":{\"type\":\"integer\","
"\"description\":\"xorshift128+ seed for reproducibility. 0 = non-deterministic. Default: 0\"},"
- "\"n_periods_per_year\":{\"type\":\"integer\","
+ "\"n_periods_per_year\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":10000000,"
"\"description\":\"Trading periods per year — controls GBM time step dt=1/n_periods_per_year. "
"Default: 252. Must match the annualization basis of expected_return and volatility\"},"
- "\"percentiles\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},"
- "\"description\":\"Percentile tail levels for VaR, e.g. [0.01, 0.05] for 99%% and 95%% VaR. "
- "Max 8 values. Default: [0.01, 0.05]\"},"
- "\"model\":{\"type\":\"string\",\"enum\":[\"gbm\",\"merton\"],"
+ "\"percentiles\":{\"type\":\"array\",\"maxItems\":8,"
+ "\"items\":{\"type\":\"number\",\"exclusiveMinimum\":0,\"exclusiveMaximum\":1},"
+ "\"description\":\"Percentile tail levels for VaR, e.g. [0.01, 0.05] for 99% and 95% VaR. "
+ "Max 8 values, each strictly between 0 and 1. Default: [0.01, 0.05]\"},"
+ "\"model\":{\"type\":\"string\",\"enum\":[\"gbm\",\"merton\"],\"default\":\"gbm\","
"\"description\":\"gbm (default) or merton jump-diffusion\"},"
"\"jump_intensity\":{\"type\":\"number\",\"description\":\"Merton: jumps per year. Default 1.0\"},"
"\"jump_mean\":{\"type\":\"number\",\"description\":\"Merton: mean log-jump. Default -0.03\"},"
@@ -182,7 +189,7 @@ static const char SCHEMA_MONTE_CARLO[] =
"\"weights\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},"
"\"description\":\"Correlated book: n_assets weights, each >= 0 (long-only), summing to 1 unless "
"normalize_weights=true\"},"
- "\"n_assets\":{\"type\":\"integer\","
+ "\"n_assets\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":100,"
"\"description\":\"Correlated book: asset count (max 100). Inferred from weights if omitted\"},"
"\"expected_returns\":{\"type\":\"array\",\"items\":{\"type\":\"number\"},"
"\"description\":\"Correlated book: per-asset annualized drifts. Default: expected_return for every asset\"},"
@@ -200,19 +207,21 @@ static const char SCHEMA_SCENARIO_ANALYSIS[] =
"{"
"\"type\":\"object\","
"\"properties\":{"
+ "\"n_assets\":{\"type\":\"integer\",\"minimum\":1,\"maximum\":3000,"
+ "\"description\":\"Optional; if given it must equal the length of assets\"},"
"\"assets\":{"
- "\"type\":\"array\","
+ "\"type\":\"array\",\"minItems\":1,\"maxItems\":3000,"
"\"description\":\"Portfolio assets, each with its scenario list\","
"\"items\":{"
"\"type\":\"object\","
"\"properties\":{"
"\"asset_id\":{\"type\":\"integer\","
- "\"description\":\"Unique asset identifier\"},"
- "\"current_price\":{\"type\":\"number\","
+ "\"description\":\"Unique asset identifier. Default: 1-based position in assets\"},"
+ "\"current_price\":{\"type\":\"number\",\"exclusiveMinimum\":0,"
"\"description\":\"Current market price in currency units\"},"
- "\"position_size\":{\"type\":\"number\","
+ "\"position_size\":{\"type\":\"number\",\"exclusiveMinimum\":0,"
"\"description\":\"Units held, > 0 (long only). Scales upside/downside to dollar terms and weights the portfolio expected return\"},"
- "\"scenarios\":{\"type\":\"array\",\"minItems\":2,"
+ "\"scenarios\":{\"type\":\"array\",\"minItems\":1,\"maxItems\":10,"
"\"description\":\"One entry per scenario. Probabilities must sum to 1.0\","
"\"items\":{\"type\":\"object\","
"\"properties\":{"
@@ -224,14 +233,12 @@ static const char SCHEMA_SCENARIO_ANALYSIS[] =
"\"required\":[\"price\",\"probability\"]"
"}}"
"},"
- "\"required\":[\"asset_id\",\"current_price\",\"position_size\",\"scenarios\"]"
+ "\"required\":[\"current_price\",\"position_size\",\"scenarios\"]"
"}"
"},"
- "\"use_hash_lookup\":{\"type\":\"boolean\","
- "\"description\":\"Benchmark O(1) HashMap lookup against O(n) linear scan. Default: true\"},"
- "\"prob_tolerance\":{\"type\":\"number\","
+ "\"prob_tolerance\":{\"type\":\"number\",\"exclusiveMinimum\":0,\"maximum\":0.1,\"default\":0.0001,"
"\"description\":\"Tolerance for probability sum validation per asset. "
- "Default: 1e-4 (0.01%%). Clamped to [1e-10, 0.1]\"},"
+ "Default: 1e-4 (0.01%). Must be > 0 and <= 0.1\"},"
"\"request_id\":{\"type\":\"string\"}"
"},"
"\"required\":[\"assets\"]"
@@ -280,7 +287,7 @@ static const finbench_mcp_tool_t finbench_mcp_tools[] = {
"monteCarlo",
"Monte Carlo Value at Risk (VaR) simulation using Geometric Brownian Motion: "
"S(t+dt) = S(t) * exp((mu - sigma^2/2)*dt + sigma*sqrt(dt)*Z), where Z ~ N(0,1). "
- "Returns VaR at caller-specified percentiles, CVaR (Expected Shortfall at 95%%), "
+ "Returns VaR at caller-specified percentiles, CVaR (Expected Shortfall at 95%), "
"max drawdown, probability of profit, and simulations-per-second throughput. "
"Two modes: a single asset (volatility) or, when covariance_matrix + weights are supplied, "
"a correlated buy-and-hold book whose per-step shocks are L*Z with L the Cholesky factor of "