canli-validation-mcp 0.8.2 → 0.9.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/README.md CHANGED
@@ -333,11 +333,11 @@ tokenizers give different absolute counts), in the shape an OpenAI-style client
333
333
 
334
334
  | CANLI_TOOLSETS | tools | tokens per turn | of all |
335
335
  |---|---|---|---|
336
- | `all` | 14 | 3,888 | 100% |
337
- | `validate` | 10 | 3,154 | 81% |
338
- | `receipts` | 2 | 340 | 9% |
339
- | `company` | 1 | 302 | 8% |
340
- | `status` | 1 | 98 | 3% |
336
+ | `all` | 14 | 3,834 | 100% |
337
+ | `validate` | 10 | 3,166 | 83% |
338
+ | `receipts` | 2 | 312 | 8% |
339
+ | `company` | 1 | 273 | 7% |
340
+ | `status` | 1 | 89 | 2% |
341
341
 
342
342
  Providers cache a tool list that is identical from turn to turn and bill the cached part at a
343
343
  fraction of the price (`test/tool-list-stable.test.mjs` keeps each list byte-stable); a smaller list
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canli-validation-mcp",
3
- "version": "0.8.2",
3
+ "version": "0.9.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",
@@ -58,22 +58,60 @@ export function bookSharpe({ sleeveSharpe, sleeves, correlation }) {
58
58
  return sleeveSharpe * Math.sqrt(sleeves / denominator);
59
59
  }
60
60
 
61
- /** The value no amount of breadth can exceed. Infinite only when rho <= 0. */
61
+ /**
62
+ * The most sleeves a shared correlation admits: `1 + (N-1)*rho` must stay positive, so a negative
63
+ * rho allows only N < 1 - 1/rho (rho = -0.3 allows 4). Infinite when rho >= 0, and when rho is so
64
+ * close to zero (above about -1e-16) that the cap passes 2^53 sleeves.
65
+ */
66
+ export function maxSleeves(correlation) {
67
+ if (!(correlation < 0)) return Number.POSITIVE_INFINITY;
68
+ const bound = 1 - 1 / correlation;
69
+ if (!(bound < Number.MAX_SAFE_INTEGER)) return Number.POSITIVE_INFINITY;
70
+ let n = Math.max(1, Math.ceil(bound) - 1);
71
+ // The closed form, then one step either way against floating-point rounding at the boundary.
72
+ while (n > 1 && 1 + (n - 1) * correlation <= 0) n -= 1;
73
+ while (1 + n * correlation > 0) n += 1;
74
+ return n;
75
+ }
76
+
77
+ /**
78
+ * The most a book of these sleeves can be worth. For rho > 0 it is the limit s / sqrt(rho), which
79
+ * no finite book reaches. For rho < 0 it is the book at maxSleeves, which is reached: a negative
80
+ * shared correlation caps the count, so it caps the Sharpe too. Infinite when rho is 0 (and, in
81
+ * practice, within about 1e-16 below it).
82
+ */
62
83
  export function breadthCeiling({ sleeveSharpe, correlation }) {
63
- if (correlation <= 0) return Number.POSITIVE_INFINITY;
64
- return sleeveSharpe / Math.sqrt(correlation);
84
+ if (correlation > 0) return sleeveSharpe / Math.sqrt(correlation);
85
+ const cap = maxSleeves(correlation);
86
+ if (!Number.isFinite(cap)) return Number.POSITIVE_INFINITY;
87
+ return bookSharpe({ sleeveSharpe, sleeves: cap, correlation });
65
88
  }
66
89
 
67
- /** The smallest N reaching `target`, or null when the ceiling forbids it. */
68
- export function sleevesRequired({ sleeveSharpe, correlation, target, maxSleeves = 500 }) {
90
+ /**
91
+ * The smallest N reaching `target`, or null when no admissible N does. Exact at any size: from
92
+ * s * sqrt(N / (1 + (N-1)*rho)) >= T, with k = (T/s)^2, N * (1 - k*rho) >= k * (1 - rho). A target
93
+ * of 50 from sleeves of Sharpe 1 at rho = 0.0001 needs 3,333 sleeves; a search that stopped at 500
94
+ * called it unreachable.
95
+ */
96
+ export function sleevesRequired({ sleeveSharpe, correlation, target }) {
69
97
  const ceiling = breadthCeiling({ sleeveSharpe, correlation });
70
- if (target > ceiling) return { sleeves: null, ceiling, reachable: false };
71
- for (let n = 1; n <= maxSleeves; n += 1) {
72
- if (bookSharpe({ sleeveSharpe, sleeves: n, correlation }) >= target) {
73
- return { sleeves: n, ceiling, reachable: true };
74
- }
98
+ const unreachable = { sleeves: null, ceiling, reachable: false };
99
+ if (!(sleeveSharpe > 0) || !(target > 0)) return unreachable;
100
+ if (target <= sleeveSharpe) return { sleeves: 1, ceiling, reachable: true };
101
+ // For rho > 0 the ceiling is a limit no book attains; for rho < 0 it is attained at maxSleeves.
102
+ if (correlation > 0 ? target >= ceiling : target > ceiling) return unreachable;
103
+ const k = (target / sleeveSharpe) ** 2;
104
+ const book = (n) => bookSharpe({ sleeveSharpe, sleeves: n, correlation });
105
+ let n = Math.max(1, Math.ceil((k * (1 - correlation)) / (1 - k * correlation)));
106
+ if (!Number.isSafeInteger(n)) return unreachable;
107
+ const cap = maxSleeves(correlation);
108
+ if (n > cap) n = cap;
109
+ while (n > 1 && book(n - 1) >= target) n -= 1;
110
+ while (book(n) < target) {
111
+ if (n >= cap || !Number.isSafeInteger(n + 1)) return unreachable;
112
+ n += 1;
75
113
  }
76
- return { sleeves: null, ceiling, reachable: false };
114
+ return { sleeves: n, ceiling, reachable: true };
77
115
  }
78
116
 
79
117
  /** The curve of book Sharpe against N, for plotting. */
@@ -1,7 +1,7 @@
1
1
  // js/validate/breadth.js
2
2
  // The pure computation behind POST /api/v1/validate/breadth, 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 { bookSharpe, breadthCeiling, ceilingCaptured, sleevesRequired } from "../breadth-core.js";
4
+ import { bookSharpe, breadthCeiling, ceilingCaptured, maxSleeves, sleevesRequired } from "../breadth-core.js";
5
5
 
6
6
  export function compute(body) {
7
7
  const sleeveSharpe = Number(body.sleeve_sharpe);
@@ -13,12 +13,40 @@ export function compute(body) {
13
13
  const target = body.target === undefined ? null : Number(body.target);
14
14
  if (target !== null && !(target > 0)) throw new RangeError("target must be a positive Sharpe");
15
15
  const ceiling = breadthCeiling({ sleeveSharpe, correlation });
16
- const out = { identity: "S_book = s_bar * sqrt(N / (1 + (N - 1) * rho_bar)); ceiling as N grows is s_bar / sqrt(rho_bar)", ceiling: Number.isFinite(ceiling) ? ceiling : null, ceiling_is_unbounded: !Number.isFinite(ceiling) };
17
- if (sleeves !== null) out.book = { sleeves, book_sharpe: bookSharpe({ sleeveSharpe, sleeves, correlation }), ceiling_captured: Number.isFinite(ceiling) ? ceilingCaptured({ sleeveSharpe, sleeves, correlation }) : null };
16
+ const cap = maxSleeves(correlation);
17
+ const bounded = Number.isFinite(ceiling);
18
+ // A negative shared correlation admits only so many sleeves, so the ceiling is the book at that
19
+ // count and is reached; a positive one gives a limit no finite book reaches; zero gives none.
20
+ const kind = !bounded ? "unbounded" : correlation > 0 ? "limit" : "maximum";
21
+ const out = {
22
+ identity: "S_book = s_bar * sqrt(N / (1 + (N - 1) * rho_bar)); for rho_bar > 0 the ceiling is the limit s_bar / sqrt(rho_bar); for rho_bar < 0 only N < 1 - 1 / rho_bar is possible, and the ceiling is the book at the largest such N",
23
+ ceiling: bounded ? ceiling : null,
24
+ ceiling_is_unbounded: !bounded,
25
+ ceiling_kind: kind,
26
+ ...(Number.isFinite(cap) ? { max_sleeves: cap } : {}),
27
+ };
28
+ if (sleeves !== null) {
29
+ if (sleeves > cap) throw new RangeError(`a shared correlation of ${correlation} admits at most ${cap} sleeves: with more, 1 + (N - 1) * rho is not positive and no real set of returns has that correlation`);
30
+ out.book = { sleeves, book_sharpe: bookSharpe({ sleeveSharpe, sleeves, correlation }), ceiling_captured: bounded ? ceilingCaptured({ sleeveSharpe, sleeves, correlation }) : null };
31
+ }
18
32
  if (target !== null) {
19
33
  const req = sleevesRequired({ sleeveSharpe, correlation, target });
20
- out.target = { target, reachable: req.reachable, sleeves_required: req.sleeves, note: req.reachable ? "At this per-sleeve quality and correlation the target is reachable with the stated sleeve count." : "No number of sleeves of this quality at this correlation reaches the target; raise per-sleeve quality or lower correlation." };
34
+ const n = req.sleeves;
35
+ out.target = {
36
+ target,
37
+ reachable: req.reachable,
38
+ sleeves_required: n,
39
+ note: req.reachable
40
+ ? `${n} sleeve${n === 1 ? "" : "s"} of this quality at this correlation reach a book Sharpe of ${target}; ${n === 1 ? "one sleeve already does" : `${n - 1} fall short`}.`
41
+ : kind === "maximum"
42
+ ? `At most ${cap} sleeves can share this correlation, and ${cap} of them reach only ${ceiling.toFixed(3)}; raise per-sleeve quality.`
43
+ : "No number of sleeves of this quality at this correlation reaches the target; raise per-sleeve quality or lower correlation.",
44
+ };
21
45
  }
22
- out.plain_reading = out.ceiling_is_unbounded ? "With average pairwise correlation at or below zero the book Sharpe has no ceiling from breadth alone; correlation this low is rare and should be checked in stress." : `Adding sleeves of this quality can never take the book above a Sharpe of ${ceiling.toFixed(3)}. Quality and correlation set the ceiling; count only approaches it.`;
46
+ out.plain_reading = kind === "unbounded"
47
+ ? "With average pairwise correlation of zero the book Sharpe grows with the square root of the sleeve count and has no ceiling from breadth alone; independence this clean is rare and should be checked in stress."
48
+ : kind === "maximum"
49
+ ? `A shared correlation of ${correlation} is possible for at most ${cap} sleeves, and ${cap} of them are worth a book Sharpe of ${ceiling.toFixed(3)}; a negative average correlation this strong is rare and should be checked in stress.`
50
+ : `Adding sleeves of this quality can never take the book above a Sharpe of ${ceiling.toFixed(3)}. Quality and correlation set the ceiling; count only approaches it.`;
23
51
  return out;
24
52
  }
package/src/schemas.mjs CHANGED
@@ -14,44 +14,44 @@ import { z } from "zod";
14
14
  export const FIELD_DESCRIPTIONS = Object.freeze({
15
15
  observed_sharpe_annualized: "Annualized Sharpe as observed.",
16
16
  observations: "Number of return observations.",
17
- periods_per_year: "Observations per year: 252 daily, 365 daily crypto, 52 weekly, 12 monthly.",
17
+ periods_per_year: "Periods per year: 252 daily, 365 crypto, 52 weekly, 12 monthly.",
18
18
  skew: "Skewness of returns; 0 if Normal.",
19
19
  non_excess_kurtosis: "Kurtosis, not excess kurtosis; 3 if Normal.",
20
20
  effective_independent_trials: "Independent variants tried before choosing this one.",
21
- cross_trial_sharpe_sd_annualized: "Standard deviation of the annualized Sharpe across those trials.",
22
- returns: "Periodic returns as fractions (0.01 is 1%), oldest first; replaces the Sharpe, observations, skew and kurtosis fields.",
23
- matrix: "Returns as fractions: one row per period, one column per variant.",
21
+ cross_trial_sharpe_sd_annualized: "Standard deviation of annualized Sharpe across those trials.",
22
+ returns: "Periodic returns as fractions (0.01 = 1%), oldest first; replaces the Sharpe, observations, skew and kurtosis.",
23
+ matrix: "Returns as fractions, one row per period, one column per variant.",
24
24
  n_splits: "Even number of blocks, at least 2; default 16.",
25
25
  max_combinations: "Most splits evaluated, up to 2000 (default).",
26
26
  seed: "Sampling seed; default 42.",
27
27
  record: "A canli.paper-evidence.v0 record.",
28
28
  sleeve_sharpe: "Annualized Sharpe of one sleeve.",
29
29
  average_pairwise_correlation: "Average correlation between sleeves, -1 to 1.",
30
- sleeves: "Sleeve count, to get that book's Sharpe.",
31
- target: "Target book Sharpe, to get the sleeves it needs.",
30
+ sleeves: "Sleeve count, for that book's Sharpe.",
31
+ target: "Target book Sharpe, for the sleeves it needs.",
32
32
  benchmark_sharpe_annualized: "Annualized Sharpe to beat; default 0.",
33
33
  confidence: "Between 0 and 1; default 0.95.",
34
- record_observations: "Record length so far, to get its probabilistic Sharpe.",
34
+ record_observations: "Record length so far, for its probabilistic Sharpe.",
35
35
  label: "Name for the key.",
36
36
  receipt_id: "Receipt id from a validation result.",
37
- receipt_object: "A receipt as get_receipt returns it (its data), to verify without fetching it.",
37
+ receipt_object: "A receipt as get_receipt returns it, to verify without fetching.",
38
38
  cik: "SEC CIK; send cik or ticker.",
39
39
  ticker: "Ticker such as AAPL; send ticker or cik.",
40
40
  concept: "us-gaap concept such as Assets; omit to list them.",
41
41
  limit: "Most observations, newest first; default 40.",
42
- target_sharpe_annualized: "In-sample annualized Sharpe you would take as a discovery; default 1.",
43
- backtest_years: "Length of the backtest in years, to get the most independent trials it allows.",
44
- bt_trials: "Independent trials (backtests, parameter sets, ideas) tried; gives the minimum backtest length.",
45
- hc_observations: "Number of return observations behind the Sharpe ratio.",
46
- hc_tests: "Total tests run, this one included; gives the Bonferroni and independent-test haircuts.",
47
- autocorrelation: "First-order autocorrelation of the returns, -1 to 1; default 0. Corrects the annualized Sharpe as Lo (2002).",
48
- other_sharpes: "Annualized Sharpe ratios of the other tests, over the same observations; adds the Holm and BHY haircuts.",
49
- lt_trials: "Independent trials tried; adds the chance that the best of them reached this Sharpe by luck.",
50
- lt_skew: "Skewness of the returns; below -0.5 the reading warns that the counts are too generous.",
51
- variants: "Optional returns of every variant tried, this one included, as fractions: one row per period, one column per variant. Adds the overfitting check.",
52
- returns_file: "Path to a CSV or JSON file of the returns on the machine running this server, instead of returns. Not available on the hosted endpoint.",
53
- returns_column: "Header name or 1-based position of the returns column when returns_file has several numeric columns.",
54
- variants_file: "Path to a CSV or JSON file of every variant's returns (one numeric column per variant), instead of variants.",
42
+ target_sharpe_annualized: "In-sample annualized Sharpe you would call a discovery; default 1.",
43
+ backtest_years: "Backtest length in years, for the most trials it allows.",
44
+ bt_trials: "Independent trials tried (backtests, parameter sets, ideas).",
45
+ hc_observations: "Return observations behind the Sharpe.",
46
+ hc_tests: "Tests run, this one included; gives the Bonferroni and independent-test haircuts.",
47
+ autocorrelation: "Lag-1 autocorrelation of returns, -1 to 1; default 0. Corrects the annualized Sharpe (Lo 2002).",
48
+ other_sharpes: "Annualized Sharpes of the other tests over the same observations; adds Holm and BHY.",
49
+ lt_trials: "Independent trials tried; adds the chance the best reached this Sharpe by luck.",
50
+ lt_skew: "Skewness; below -0.5 the reading warns the counts are too generous.",
51
+ variants: "Optional returns of every variant tried (this one included), one row per period, one column per variant; adds the overfitting check.",
52
+ returns_file: "Path to a CSV or JSON of the returns on this machine (not on the hosted endpoint), instead of returns.",
53
+ returns_column: "Column name or 1-based position, when returns_file has several numeric columns.",
54
+ variants_file: "Path to a CSV or JSON with one numeric column per variant, instead of variants.",
55
55
  });
56
56
  const d = FIELD_DESCRIPTIONS;
57
57
 
@@ -302,20 +302,20 @@ export const REGISTRY_DESCRIPTION_MAX = 100;
302
302
  // above) what the tool's result cannot be used to claim, so an agent sees this before it ever
303
303
  // calls the tool, not only inside the returned envelope.
304
304
  export const TOOL_DESCRIPTIONS = Object.freeze({
305
- get_key: `Issue a free canlicapital.com validation key (POST /api/v1/keys) and hold it in memory for this session. Only needed before a validation when neither CANLI_KEY nor local mode is set; the read tools (get_receipt, service_status, company_financial_history) never need a key. ${LIMITS_SENTENCES.quotas}`,
306
- validate_deflated_sharpe: `Deflated Sharpe ratio: the probability (0 to 1) that one selected strategy's Sharpe beats the best Sharpe that luck alone would give across the variants tried, with the probabilistic Sharpe and that luck benchmark. Use it for the strategy you kept after a search, when you know how many variants were tried and how their Sharpe ratios spread; send the seven statistics or a return series, not both. With every variant's returns use validate_overfitting; to state luck as a trial count use validate_luck_trials; for a multiple-testing haircut use validate_haircut_sharpe. ${LIMITS_SENTENCES.notAdmission}`,
307
- audit_backtest: `Audit one strategy's return series in one call: deflated Sharpe, the minimum track record length for its Sharpe to beat the benchmark, and, with every variant's returns, the probability of backtest overfitting. Point returns_file at the backtest's CSV or JSON rather than copying long series into the call. Each check is the matching validate_ tool's result with its own receipt, side by side; the audit does not grade the strategy. Prefer it to calling those validators one by one when you have one strategy's return series. Uses one validation per check. ${LIMITS_SENTENCES.notAdmission}`,
308
- validate_overfitting: `Probability of backtest overfitting (0 to 1) by CSCV: how often the variant that is best in sample falls below the median out of sample. Use it when you have every variant's returns as a matrix (periods by variants); with summary statistics only, use validate_deflated_sharpe. ${LIMITS_SENTENCES.notAdmission}`,
309
- validate_paper_evidence: `Whether a paper or simulated performance record meets canli.paper-evidence.v0, with each failure's JSON pointer. Use it on a record document before publishing or relying on it: it checks structure and required disclosures, not whether the returns are good. ${LIMITS_SENTENCES.scope}`,
310
- validate_backtest_length: `Minimum backtest length, in years, before the best of N independent trials is not expected to reach a target Sharpe by luck, and with backtest_years, the most independent trials those years allow. Send effective_independent_trials, backtest_years, or both. Use it before or while planning a search, to size the backtest for the number of trials; once a search has a result, use validate_deflated_sharpe. ${LIMITS_SENTENCES.notAdmission}`,
311
- validate_luck_trials: `How many skill-less strategies a search would have had to try for its best to reach this Sharpe by luck, and, with a trial count, the chance that it did. Calibrated by Monte Carlo. Use it to state luck as a count ("as good as the best of N random tries") or to test a result against the trials actually run; for a probability that the Sharpe is real, use validate_deflated_sharpe. ${LIMITS_SENTENCES.notAdmission}`,
312
- validate_haircut_sharpe: `Haircut Sharpe ratio for multiple testing (Harvey and Liu, 2015): the Sharpe a single test would have needed once the number of tests is counted, by Bonferroni and for independent tests, and with the other tests' Sharpe ratios by Holm and BHY. Use it to report a Sharpe adjusted for the number of tests, as finance papers do; for a probability that the Sharpe is real, use validate_deflated_sharpe. ${LIMITS_SENTENCES.notAdmission}`,
313
- validate_track_record: `Minimum track record length, in observations and years, for an observed Sharpe to beat a benchmark at a confidence level, and with observations, the record's probabilistic Sharpe so far. Use it for a live or paper record: how long it must run before its Sharpe is evidence. To size a backtest against the number of trials, use validate_backtest_length. ${LIMITS_SENTENCES.notAdmission}`,
314
- validate_breadth: `Highest book Sharpe reachable by adding sleeves of this quality and correlation, the Sharpe at a sleeve count, and the sleeves a target needs. Use it for portfolio construction, to see what adding strategies can and cannot do; it validates no single strategy. ${LIMITS_SENTENCES.scope}`,
315
- verify_receipt: `Check a validation receipt's Ed25519 signature offline against the canlicapital.com public key bundled in this package, that its output hashes to its output_sha256, and that its content hashes to its id. Send an id to fetch the receipt first, or a receipt already fetched. ${LIMITS_SENTENCES.unsigned}`,
316
- get_receipt: `Fetch a stored verdict by receipt id (GET /api/v1/receipts/{id}) to re-read an earlier result. No key. To check that the receipt is genuine, use verify_receipt. ${LIMITS_SENTENCES.unsigned}`,
317
- service_status: `Whether the validation API is up, with quota constants (GET /api/v1/validate/status); check after a timeout before resubmitting. No key. ${LIMITS_SENTENCES.scope}`,
318
- company_financial_history: `SEC-reported financial history for one company from the canlicapital.com company reference (GET /company-data/{cik}.json), by cik or by ticker (resolved through GET /api/v1/company-tickers.json, companies in the release only). Without a concept it lists the available histories; with one it returns observations, newest first, each with its filing accession, form, filed date and unit, plus the SHA-256 of the original SEC response. No key required. ${COMPANY_REFERENCE_BOUNDARY}`,
305
+ get_key: `Issue a free validation key for this session. Rarely needed: the first validation issues one itself unless CANLI_KEY or local mode is set, and the read tools need none. ${LIMITS_SENTENCES.quotas}`,
306
+ validate_deflated_sharpe: `Deflated Sharpe ratio: the probability (0 to 1) that the selected strategy's Sharpe beats the best that luck gives across the variants tried, with the probabilistic Sharpe and that luck benchmark. Send the seven statistics or a return series. With every variant's returns use validate_overfitting; luck as a trial count, validate_luck_trials; a multiple-testing haircut, validate_haircut_sharpe. ${LIMITS_SENTENCES.notAdmission}`,
307
+ audit_backtest: `One-call audit of a strategy's returns: deflated Sharpe, minimum track record and, with every variant's returns, the probability of backtest overfitting, each the matching validator's result with its own receipt. Point returns_file at the backtest's CSV or JSON instead of pasting long series. Prefer it to calling the validators one by one; one validation per check. ${LIMITS_SENTENCES.notAdmission}`,
308
+ validate_overfitting: `Probability of backtest overfitting (0 to 1) by CSCV: how often the in-sample best variant falls below the out-of-sample median. Needs every variant's returns (periods by variants); with summary statistics only, use validate_deflated_sharpe. ${LIMITS_SENTENCES.notAdmission}`,
309
+ validate_paper_evidence: `Whether a paper or simulated performance record meets canli.paper-evidence.v0, with a JSON pointer per failure. Checks structure and required disclosures, not whether the returns are good. ${LIMITS_SENTENCES.scope}`,
310
+ validate_backtest_length: `Minimum backtest length (years) before the best of N independent trials is not expected to reach a target Sharpe by luck; with backtest_years, the most trials those years allow. For planning a search; once it has a result, use validate_deflated_sharpe. ${LIMITS_SENTENCES.notAdmission}`,
311
+ validate_luck_trials: `How many skill-less strategies a search would need for its best to reach this Sharpe by luck (Monte Carlo), and with a trial count, the chance it did. States luck as the best of N random tries; for the probability the Sharpe is real, use validate_deflated_sharpe. ${LIMITS_SENTENCES.notAdmission}`,
312
+ validate_haircut_sharpe: `Haircut Sharpe for multiple testing (Harvey and Liu 2015): the Sharpe a single test would have needed, by Bonferroni and independent tests, and with the other tests' Sharpes, Holm and BHY. For the probability the Sharpe is real, use validate_deflated_sharpe. ${LIMITS_SENTENCES.notAdmission}`,
313
+ validate_track_record: `Minimum track record length (observations and years) for an observed Sharpe to beat a benchmark at a confidence level; with observations, the record's probabilistic Sharpe so far. For live or paper records; to size a backtest for its trials, use validate_backtest_length. ${LIMITS_SENTENCES.notAdmission}`,
314
+ validate_breadth: `Book Sharpe ceiling from adding sleeves of this quality and correlation, the Sharpe at a sleeve count, and the sleeves a target needs. For portfolio construction; it validates no single strategy. ${LIMITS_SENTENCES.scope}`,
315
+ verify_receipt: `Verify a receipt offline: its Ed25519 signature against the bundled canlicapital.com key, its output hash and its id. Send an id to fetch it first, or the receipt itself. ${LIMITS_SENTENCES.unsigned}`,
316
+ get_receipt: `Fetch a stored verdict by receipt id to re-read it. No key; verify_receipt checks it is genuine. ${LIMITS_SENTENCES.unsigned}`,
317
+ service_status: `Whether the validation API is up, with its quotas; check after a timeout before resubmitting. No key. ${LIMITS_SENTENCES.scope}`,
318
+ company_financial_history: `SEC-reported financial history for one company in the canlicapital.com reference, by cik or ticker: without a concept, the histories available; with one, observations newest first with accession, form, filed date and unit, plus the source's SHA-256. For point-in-time values use canli-fundamentals-mcp. ${COMPANY_REFERENCE_BOUNDARY}`,
319
319
  });
320
320
 
321
321
  // Output schemas (0.8.0). Published OPEN: extra fields always pass. A client validates a tool's
package/src/server.mjs CHANGED
@@ -19,6 +19,10 @@ import { breadthInput, trackRecordInput, auditBacktestInput, verifyReceiptToolSh
19
19
  export const DEFAULT_BASE = "https://canlicapital.com";
20
20
  export const SERVER_NAME = "canlicapital-validation-mcp";
21
21
  export const SERVER_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
22
+ // Sent once in initialize; clients such as Claude Code put it in the system prompt, so the model
23
+ // knows the first call to make even when tool definitions are deferred. Byte-stable across runs.
24
+ export const SERVER_INSTRUCTIONS = "Checks whether a backtest's result is real. For one strategy's returns, call audit_backtest, pointing returns_file at the backtest's CSV instead of pasting long series. For a Sharpe found by a search, validate_deflated_sharpe needs how many independent variants were tried and how their Sharpes spread; with every variant's returns, validate_overfitting gives the probability of backtest overfitting. Set CANLI_LOCAL=1 to compute on this machine with no network. Every result says what it does not establish; quote those limits with the number.";
25
+
22
26
  // How the server introduces itself in initialize: a readable title, the page that documents it
23
27
  // and its icon, so clients and directories that read serverInfo show more than a package name.
24
28
  export const SERVER_INFO = Object.freeze({
@@ -705,7 +709,7 @@ export function registerAll(server, session) {
705
709
  }
706
710
 
707
711
  export function createServer(session = createSession()) {
708
- const server = new McpServer(SERVER_INFO);
712
+ const server = new McpServer(SERVER_INFO, { instructions: SERVER_INSTRUCTIONS });
709
713
  registerAll(server, session);
710
714
  return server;
711
715
  }