canli-validation-mcp 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canli-validation-mcp",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Check whether a backtest is real: deflated Sharpe, CSCV probability of backtest overfitting, minimum track record and backtest length, haircut Sharpe and luck-equivalent trials, as an MCP server. Local mode, hosted endpoint, signed receipts.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -7,9 +7,23 @@
|
|
|
7
7
|
// standards/validation-api/vectors.json.
|
|
8
8
|
import { calculateDsr } from "./dsr-core.js";
|
|
9
9
|
|
|
10
|
+
// JSON has no NaN, so a missing value arrives as null, and Number(null) is 0: read that way, a gap
|
|
11
|
+
// becomes a period of zero return, and a malformed string used to be dropped without a word. Each
|
|
12
|
+
// entry must be a finite number or a numeric string; anything else is refused by its position.
|
|
13
|
+
export function finiteNumbers(values, name) {
|
|
14
|
+
if (!Array.isArray(values)) throw new RangeError(`${name} must be an array of numbers`);
|
|
15
|
+
const out = new Array(values.length);
|
|
16
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
17
|
+
const v = values[i];
|
|
18
|
+
const x = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : Number.NaN;
|
|
19
|
+
if (!Number.isFinite(x)) throw new RangeError(`${name}[${i}] is ${v === null ? "null" : v === undefined ? "missing" : typeof v === "number" ? String(v) : JSON.stringify(v)}, not a finite number: remove missing periods rather than sending them, since they would otherwise be read as zero`);
|
|
20
|
+
out[i] = x;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
10
25
|
export function perPeriodMoments(returns) {
|
|
11
|
-
|
|
12
|
-
const x = returns.map(Number).filter((v) => Number.isFinite(v));
|
|
26
|
+
const x = finiteNumbers(returns, "returns");
|
|
13
27
|
const n = x.length;
|
|
14
28
|
if (n < 2) throw new RangeError("need at least 2 finite return observations");
|
|
15
29
|
let min = Infinity;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// js/validate/haircut-sharpe.js
|
|
2
2
|
// The pure computation behind POST /api/v1/validate/haircut-sharpe, shared by the API route and the
|
|
3
3
|
// MCP package's local mode (mcp/src/local mirrors it byte for byte), so the two cannot disagree.
|
|
4
|
+
import { finiteNumbers } from "../moments-core.js";
|
|
4
5
|
import { haircutSharpe } from "../haircut-core.js";
|
|
5
6
|
|
|
6
7
|
// The haircut Sharpe ratio of Harvey and Liu, "Backtesting" (2015), checked against the authors'
|
|
@@ -13,7 +14,7 @@ const MAX_OTHERS = 10000;
|
|
|
13
14
|
export function compute(body) {
|
|
14
15
|
const missing = ["observed_sharpe_annualized", "periods_per_year", "observations"].filter((k) => body[k] === undefined);
|
|
15
16
|
if (missing.length) throw new RangeError(`Missing required fields: ${missing.join(", ")}`);
|
|
16
|
-
const others = body.other_sharpe_ratios_annualized;
|
|
17
|
+
const others = body.other_sharpe_ratios_annualized === undefined || !Array.isArray(body.other_sharpe_ratios_annualized) ? body.other_sharpe_ratios_annualized : finiteNumbers(body.other_sharpe_ratios_annualized, "other_sharpe_ratios_annualized");
|
|
17
18
|
if (others !== undefined && (!Array.isArray(others) || others.length < 1 || others.length > MAX_OTHERS)) {
|
|
18
19
|
throw new RangeError(`other_sharpe_ratios_annualized must be an array of 1 to ${MAX_OTHERS} Sharpe ratios`);
|
|
19
20
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// js/validate/overfitting.js
|
|
2
2
|
// The pure computation behind POST /api/v1/validate/overfitting, shared by the API route and the
|
|
3
3
|
// MCP package's local mode (mcp/src/local mirrors it byte for byte), so the two cannot disagree.
|
|
4
|
+
import { finiteNumbers } from "../moments-core.js";
|
|
4
5
|
import { pboCscv } from "../pbo-core.js";
|
|
5
6
|
import { LIMITS } from "../../api/_lib/limits.js";
|
|
6
7
|
|
|
@@ -12,7 +13,12 @@ export function compute(body) {
|
|
|
12
13
|
if (matrix.length > LIMITS.max_observations) throw new RangeError(`A matrix may hold at most ${LIMITS.max_observations} rows`);
|
|
13
14
|
if (matrix[0].length > LIMITS.max_variants) throw new RangeError(`A matrix may hold at most ${LIMITS.max_variants} variants`);
|
|
14
15
|
if (!Number.isInteger(max_combinations) || max_combinations > LIMITS.max_cscv_combinations) throw new RangeError(`max_combinations may not exceed ${LIMITS.max_cscv_combinations}`);
|
|
15
|
-
const
|
|
16
|
+
const k = matrix[0].length;
|
|
17
|
+
const rows = matrix.map((row, t) => {
|
|
18
|
+
if (!Array.isArray(row) || row.length !== k) throw new RangeError(`matrix row ${t} has ${Array.isArray(row) ? row.length : "no"} values; every row needs ${k}, one per variant`);
|
|
19
|
+
return finiteNumbers(row, `matrix[${t}]`);
|
|
20
|
+
});
|
|
21
|
+
const out = pboCscv(rows, { nSplits: Number(n_splits), maxCombinations: max_combinations, seed: Number(seed) });
|
|
16
22
|
const sorted = [...out.lambdas].sort((a, b) => a - b);
|
|
17
23
|
const q = (p) => sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))];
|
|
18
24
|
const degraded = out.is_oos_pairs.filter(([i, o]) => o < i).length / out.is_oos_pairs.length;
|